Знаете ли вы, что определение свойства — это не то же самое, что присвоение ему? Этот пост в блоге объясняет разницу и ее последствия. Это было вызвано
электронным письмом Аллена Вирфса-Брока в списке рассылки 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 со справочной информацией по этому вопросу]