Знаете ли вы, что определение свойства — это не то же самое, что присвоение ему? Этот пост в блоге объясняет разницу и ее последствия. Это было вызвано
электронным письмом Аллена Вирфса-Брока в списке рассылки es-обсудить.
Определение против присвоения
Определение. Чтобы определить свойство, используется такая функция, как
Object.defineProperty(obj, propName, propDesc)
Основное назначение этой функции — добавить
собственное (прямое) свойство в
obj , атрибуты которого
(доступный для записи и т. Д., См. Ниже) определены в
propDesc . Вторичная цель состоит в том, чтобы изменить атрибуты свойства, включая его значение.
Назначение. Чтобы присвоить свойству, используется выражение, такое как
obj.prop = value
Основная цель такого выражения — изменить значение. Перед выполнением этого изменения JavaScript проверяет цепочку прототипов
[1] объекта
obj : если где-то в объекте obj или в одном из его прототипов имеется установщик, то
присвоение является вызовом этого установщика. Назначение имеет побочный эффект создания свойства, если оно еще не существует — как собственное свойство
obj с атрибутами по умолчанию.
В следующих двух разделах более подробно рассматривается, как работают определения и назначения. Не стесняйтесь пропустить их. Вы все еще должны понимать секту. 4 «Последствия» и позже.
Резюме: атрибуты свойств и внутренние свойства
Прежде чем мы сможем объяснить, как работают определение и назначение свойств, давайте быстро рассмотрим, что такое атрибуты и внутренние свойства.
Виды недвижимости
JavaScript различает три вида свойств:
- Свойства именованного средства доступа: свойство, которое существует благодаря получателю или установщику.
- Свойства именованных данных: свойство, которое h является значением. Это самые распространенные свойства. Они включают методы.
- Внутренние свойства: используются внутри JavaScript и не доступны напрямую через язык. Однако могут быть косвенные способы доступа к ним. Пример: каждый объект имеет внутреннее свойство с именем [[Prototype]]. Вы не можете непосредственно прочитать его, но все равно получите его значение через Object.getPrototypeOf () . В то время как внутренние свойства именуются именем в квадратных скобках, они являются безымянными в том смысле, что они невидимы и не имеют обычного строкового имени свойства.
Атрибуты недвижимости
Атрибуты свойств — это поля, которые есть у каждого свойства и влияющие на его работу. Существуют следующие атрибуты:
- Все свойства:
- [[Enumerable]]: если свойство не перечисляемо, его нельзя увидеть некоторыми операциями, такими как for ... in и Object.keys () [2] .
- [[Настраиваемый]]: если свойство не настраивается, ни один из атрибутов (кроме [[Значение]]) не может быть изменен с помощью определения.
- Свойства именованных данных:
- [[Value]]: это значение свойства.
- [[Writable]]: определяет, можно ли изменить значение.
- Свойства именованного средства доступа:
- [[Get]]: содержит метод получения.
- [[Set]]: содержит метод установки.
Дескрипторы свойств
Дескриптор свойства — это набор атрибутов свойства, закодированных как объект. Например:
{ value: 123, writable: false }
Вы можете видеть, что имена свойств соответствуют именам атрибутов [[Value]] и [[Writable]]. Дескрипторы свойств используются такими функциями, как
Object.defineProperty ,
Object.getOwnPropertyDescriptor и
Object.create, которые либо изменяют, либо возвращают атрибуты свойства. Если в дескрипторе отсутствуют свойства, применяются следующие значения по умолчанию:
Имущество | Значение по умолчанию |
значение | не определено |
получить | не определено |
поставил | не определено |
записываемый | ложный |
перечислимый | ложный |
конфигурируемый | ложный |
Внутренние свойства
Есть
несколько внутренних свойств, которыми обладают все объекты. Среди прочего, следующие четыре:
- [[Prototype]]: прототип объекта.
- [[Extensible]]: является ли этот объект расширяемым , можно ли добавить к нему новые свойства?
- [[DefineOwnProperty]]: определить свойство. Смотрите объяснение ниже.
- [[Put]]: назначить свойство. Смотрите объяснение ниже.
Детали определения и назначения
Определение свойства
Определение свойства обрабатывается внутренним методом
[[
DefineOwnProperty ]] (P, Desc, Throw)
P это имя свойства.
Throw указывает, как операция должна
отклонить изменение: если
Throw имеет значение
true, генерируется исключение. В противном случае операция молча прерывается. Когда вызывается [[DefineOwnProperty]], выполняются следующие шаги.
- Если это не имеет собственного имущества, имя которого P : Создать новое свойство , если объект является расширяемым, отклонять , если это не так .
- В противном случае уже существует собственное свойство, и определение изменяет это свойство.
- Если это свойство не настраивается, то следующие изменения будут отклонены:
- Преобразование свойства данных в свойство средства доступа или наоборот
- Изменение [[Настраиваемый]] или [[Перечислимый]]
- Изменение [[Доступно для записи]]
- Изменение [[Value]], если [[Writable]] равно false
- Изменение [[Получить]] или [[Установить]]
- В противном случае существующее собственное свойство настраивается и может быть изменено, как указано.
Если
Desc точно отражает текущие атрибуты
этого [P], то определение никогда не отклоняется.
Двумя функциями для определения свойства являются Object.defineProperty и Object.defineProperties . Например:
Object.defineProperty(obj, propName, desc)
Внутренне это приводит к следующему вызову метода:
obj.[[DefineOwnProperty]](propName, desc, true)
Присвоение недвижимости
Присвоение свойству обрабатывается внутренним методом
[[
Put ]] (P, Value, Throw)
P и
Throw работают так же, как с [[DefineOwnProperty]]. Когда вызывается [[Put]], выполняются следующие шаги.
- Если в цепочке прототипов есть свойство только для чтения, имя которого равно P : reject.
- Если где-то в цепочке прототипов есть установщик с именем P : вызовите установщик.
- Если нет собственного свойства с именем P : если объект является расширяемым, то создайте новое свойство.
this.[[DefineOwnProperty]]( P, { value: Value, writable: true, enumerable: true, configurable: true }, Throw )
Если объект не является расширяемым, то отклонить.
- Иначе, есть собственное свойство с именем P , которое доступно для записи. Invoke
this.[[DefineOwnProperty]](P, { value: Value }, Throw)
Это обновляет значение P , но сохраняет его атрибуты (такие как перечисляемость) неизменными
Оператор присваивания (
= ) вызывает [[Put]]. Например:
obj.prop = v;
Внутренне это вызывает вызов
obj.[[Put]]("prop", v, isStrictModeOn)
То есть оператор присваивания генерирует только если выполняется в строгом режиме. [[Put]] не возвращает Value , но оператор присваивания возвращает
.
Последствия
В этом разделе описываются некоторые последствия того, как работают определение и назначение свойств.
Назначение вызывает сеттер в прототипе, определение не
Учитывая следующий пустой объект
obj , у прототипа
proto которого есть пара getter / setter, называемая
foo .
var proto = { get foo() { console.log("Getter"); return "a"; }, set foo(x) { console.log("Setter: "+x); }, }; var obj = Object.create(proto);
В чем разница между определением свойства
foo объекта
obj и назначением ему? Если вы определите, то вы намерены создать новую собственность. Они всегда создаются в первом объекте цепочки прототипов, что в данном случае означает в
obj :
> Object.defineProperty(obj, "foo", { value: "b" }); > obj.foo 'b' > proto.foo Getter 'a'
Если вместо этого вы назначаете
foo, то вы намерены изменить то, что уже существует, и это изменение должно быть обработано через установщик. И оказывается, что присваивание вызывает сеттер:
> obj.foo = "b"; Setter: b 'b'
Вы можете сделать свойство доступным только для чтения, только определив получатель. Ниже панель свойств
объекта
proto2 является таким свойством и наследуется
obj2 .
"use strict"; var proto2 = { get bar() { console.log("Getter"); return "a"; }, }; var obj2 = Object.create(proto2);
Мы используем строгий режим, чтобы при выполнении назначения было выдано исключение. В противном случае, присваивание будет просто проигнорировано (но также не изменит
obj ). При назначении мы хотим изменить
панель, которая запрещена из-за того, что
панель доступна только для чтения.
> obj2.bar = "b"; TypeError: obj.bar is read-only
Однако мы можем определить что-то новое и, таким образом, переопределить
панель свойств
proto :
> Object.defineProperty(obj2, "bar", { value: "b" }); > obj2.bar 'b' > proto2.bar Getter 'a'
Свойства только для чтения в прототипах предотвращают назначение, но не обязательно определение
Свойство только для чтения в прототипе не позволяет присваиванию добавлять собственное свойство с тем же именем, вам нужно использовать определение, если вы хотите это сделать. Это ограничение было недавно введено в ECMAScript 5.1. То есть присваивание
obj.foo = "b" не создает автоматически свойство
foo, если в одном из прототипов obj есть свойство только для чтения с таким именем
. Это продемонстрировано с помощью следующего примера, где
OBJ «s прототип
прото имеет свойство только для чтения
Foo , значение которого
„а“ .
"use strict"; var proto = Object.defineProperties( {}, { foo: { // attributes of property foo: value: "a", writable: false, // read-only configurable: true // explained later } }); var obj = Object.create(proto);
Назначение. Результатом присвоения является исключение:
> obj.foo = "b"; TypeError: obj.foo is read-only
Удивительно, что унаследованное свойство может влиять на возможность создания собственного свойства
[3] . Но это имеет смысл, потому что так же работает свойство только для получения.
Определение. С помощью определения мы хотим создать новое собственное свойство:
> Object.defineProperty(obj, "foo", { value: "b" }); > obj.foo 'b' > proto.foo 'a'
Создание свойства таким способом можно предотвратить, сделав
proto.foo дополнительно неконфигурируемым.
Оператор присваивания не меняет свойства в прототипах
Учитывая следующую настройку, где
obj наследует свойство
foo от
proto .
var proto = { foo: "a" }; var obj = Object.create(proto);
Вы не можете изменить
proto.foo , назначив
obj.foo . Это создает новое собственное свойство:
> obj.foo = "b"; 'b' > obj.foo 'b' > proto.foo 'a'
Обоснование этого поведения следующее: прототипы могут вводить свойства, значения которых являются общими для всех их потомков. Если кто-то решит изменить такое свойство у потомка, будет создано новое собственное свойство. Это означает, что можно внести изменения, но это только локально, это не влияет на других потомков. С этой точки зрения эффекты свойств только для получения и свойств только для чтения имеют смысл: предотвращать изменения, предотвращая создание собственного свойства. Какова мотивация для переопределения свойств прототипа вместо их изменения?
- Методы. Позволяют вносить исправления в методы непосредственно в прототипе, но предотвращают случайные изменения через потомков прототипа.
- Свойства, не относящиеся к методам: прототип предоставляет общие значения по умолчанию для потомков. Можно переопределить эти значения через потомка, но не изменять их. Это считается анти-паттерном и не рекомендуется. Чище назначать значения по умолчанию в конструкторах.
Только определение позволяет создать свойство с произвольными атрибутами
Если вы создаете собственное свойство с помощью присваивания, оно всегда имеет атрибуты по умолчанию. Если вы хотите указать произвольные атрибуты, вы должны использовать определение. Обратите внимание, что это включает в себя добавление геттеров и сеттеров к объекту.
Свойства литерала объекта добавляются через определение
Дан следующий объектный литерал.
var obj = { foo: 123 };
Это внутренне переведено в серию утверждений. У вас есть два варианта. Во-первых, через назначение:
var obj = new Object(); obj.foo = 123;
Во-вторых, через определение:
var obj = new Object(); Object.defineProperties(obj, { foo: { value: 123, enumerable: true, configurable: true, writable: true } });
Второй вариант лучше выражает семантику литерала объекта: для создания свежих свойств. По той же причине
Object.create получает дескрипторы свойств через свой второй аргумент.
Атрибуты методов
Одна из возможностей (предложенная Wirfs-Brock) — дать методам следующие атрибуты:
"use strict"; function Stack() { } Object.defineProperties(Stack.prototype, { push: { writable: false, configurable: true, value: function (x) { /* ... */ } } });
Идея состоит в том, чтобы предотвратить случайное назначение:
> var s = new Stack(); > s.push = 5; TypeError: s.push is read-only
Однако, поскольку
push настраивается, мы можем переопределить его в экземпляре через определение свойства.
> var s = new Stack(); > Object.defineProperty(s, "push", { value: function () { return "yes" }}) > s.push() 'yes'
Определение также позволит нам заменить
Stack.prototype.push .
Вывод
Назначение свойств часто используется для добавления новых свойств к объекту. Этот пост объяснил, что это может вызвать проблемы. Следовательно, лучше всего следовать простым правилам:
- Если вы хотите создать новое свойство, используйте определение.
- Если вы хотите изменить значение свойства, используйте присваивание.
В комментариях medikoo напоминает нам, что использование дескрипторов свойств для достижения # 1 является медленным. И я действительно часто использую назначение для создания свойств, потому что это также более удобно. К счастью, ECMAScript.next может сделать определение более быстрым и удобным: существует предложение для оператора «
определения свойств », альтернативы
Object.defineProperties . Учитывая, что разница между определением и назначением является тонкой, но важной, такая помощь в определении будет приветствоваться.
Рекомендации
- Прототипы как классы — введение в наследование JavaScript
- Свойства JavaScript: наследование и перечислимость
- Исправление ошибки запрета на переопределение только для чтения [страница в вики ECMAScript со справочной информацией по этому вопросу]