Как мы знаем, объекты могут содержать свойства.
До этого момента мы рассматривали свойство только как пару «ключ-значение». Но на самом деле свойство объекта гораздо мощнее и гибче.
В этой главе мы изучим дополнительные флаги конфигурации для свойств, а в следующей – увидим, как можно незаметно превратить их в специальные функции – геттеры и сеттеры.
Флаги свойств
Помимо значения value
, свойства объекта имеют три специальных атрибута (так называемые «флаги»).
writable
– еслиtrue
, свойство можно изменить, иначе оно только для чтения.enumerable
– еслиtrue
, свойство перечисляется в циклах, в противном случае циклы его игнорируют.configurable
– еслиtrue
, свойство можно удалить, а эти атрибуты можно изменять, иначе этого делать нельзя.
Мы ещё не встречали эти атрибуты, потому что обычно они скрыты. Когда мы создаём свойство «обычным способом», все они имеют значение true
. Но мы можем изменить их в любое время.
Сначала посмотрим, как получить их текущие значения.
Метод Object.getOwnPropertyDescriptor позволяет получить полную информацию о свойстве.
Его синтаксис:
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
- Объект, из которого мы получаем информацию.
propertyName
- Имя свойства.
Возвращаемое значение – это объект, так называемый «дескриптор свойства»: он содержит значение свойства и все его флаги.
Например:
let user = {
name: "John"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* дескриптор свойства:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
Чтобы изменить флаги, мы можем использовать метод Object.defineProperty.
Его синтаксис:
Object.defineProperty(obj, propertyName, descriptor)
obj
,propertyName
- Объект и его свойство, для которого нужно применить дескриптор.
descriptor
- Применяемый дескриптор.
Если свойство существует, defineProperty
обновит его флаги. В противном случае метод создаёт новое свойство с указанным значением и флагами; если какой-либо флаг не указан явно, ему присваивается значение false
.
Например, здесь создаётся свойство name
, все флаги которого имеют значение false
:
let user = {};
Object.defineProperty(user, "name", {
value: "John"
});
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "John",
"writable": false,
"enumerable": false,
"configurable": false
}
*/
Сравните это с предыдущим примером, в котором мы создали свойство user.name
«обычным способом»: в этот раз все флаги имеют значение false
. Если это не то, что нам нужно, надо присвоить им значения true
в параметре descriptor
.
Теперь давайте рассмотрим на примерах, что нам даёт использование флагов.
Только для чтения
Сделаем свойство user.name
доступным только для чтения. Для этого изменим флаг writable
:
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false
});
user.name = "Pete"; // Ошибка: Невозможно изменить доступное только для чтения свойство 'name'
Теперь никто не сможет изменить имя пользователя, если только не обновит соответствующий флаг новым вызовом defineProperty
.
В нестрогом режиме, без use strict
, мы не увидим никаких ошибок при записи в свойства «только для чтения» и т.п. Но эти операции всё равно не будут выполнены успешно. Действия, нарушающие ограничения флагов, в нестрогом режиме просто молча игнорируются.
Вот тот же пример, но свойство создано «с нуля»:
let user = { };
Object.defineProperty(user, "name", {
value: "John",
// для нового свойства необходимо явно указывать все флаги, для которых значение true
enumerable: true,
configurable: true
});
alert(user.name); // John
user.name = "Pete"; // Ошибка
Неперечислимое свойство
Теперь добавим собственный метод toString
к объекту user
.
Встроенный метод toString
в объектах – неперечислимый, его не видно в цикле for..in
. Но если мы напишем свой собственный метод toString
, цикл for..in
будет выводить его по умолчанию:
let user = {
name: "John",
toString() {
return this.name;
}
};
// По умолчанию оба свойства выведутся:
for (let key in user) alert(key); // name, toString
Если мы этого не хотим, можно установить для свойства enumerable:false
. Тогда оно перестанет появляться в цикле for..in
аналогично встроенному toString
:
let user = {
name: "John",
toString() {
return this.name;
}
};
Object.defineProperty(user, "toString", {
enumerable: false
});
// Теперь наше свойство toString пропало из цикла:
for (let key in user) alert(key); // name
Неперечислимые свойства также не возвращаются Object.keys
:
alert(Object.keys(user)); // name
Неконфигурируемое свойство
Флаг неконфигурируемого свойства (configurable:false
) иногда предустановлен для некоторых встроенных объектов и свойств.
Неконфигурируемое свойство не может быть удалено, его атрибуты не могут быть изменены.
Например, свойство Math.PI
– только для чтения, неперечислимое и неконфигурируемое:
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": 3.141592653589793,
"writable": false,
"enumerable": false,
"configurable": false
}
*/
То есть программист не сможет изменить значение Math.PI
или перезаписать его.
Math.PI = 3; // Ошибка, потому что writable: false
// delete Math.PI тоже не сработает
Мы также не можем изменить writable
:
// Ошибка, из-за configurable: false
Object.defineProperty(Math, "PI", { writable: true });
Мы абсолютно ничего не можем сделать с Math.PI
.
Определение свойства как неконфигурируемого – это дорога в один конец. Мы не можем изменить его обратно с помощью defineProperty
.
Обратите внимание: configurable: false
не даст изменить флаги свойства, а также не даст его удалить. При этом можно изменить значение свойства.
В коде ниже свойство user.name
является неконфигурируемым, но мы все ещё можем изменить его значение (т.к. writable: true
).
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
configurable: false
});
user.name = "Pete"; // работает
delete user.name; // Ошибка
А здесь мы делаем user.name
«навечно запечатанной» константой, как и встроенный Math.PI
:
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false,
configurable: false
});
// теперь невозможно изменить user.name или его флаги
// всё это не будет работать:
user.name = "Pete";
delete user.name;
Object.defineProperty(user, "name", { value: "Pete" });
В нестрогом режиме мы не увидим никаких ошибок при записи в свойства «только для чтения» и т.п. Эти операции всё равно не будут выполнены успешно. Действия, нарушающие ограничения флагов, в нестрогом режиме просто молча игнорируются.
Метод Object.defineProperties
Существует метод Object.defineProperties(obj, descriptors), который позволяет определять множество свойств сразу.
Его синтаксис:
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
Например:
Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
Таким образом, мы можем определить множество свойств одной операцией.
Object.getOwnPropertyDescriptors
Чтобы получить все дескрипторы свойств сразу, можно воспользоваться методом Object.getOwnPropertyDescriptors(obj).
Вместе с Object.defineProperties
этот метод можно использовать для клонирования объекта вместе с его флагами:
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
Обычно при клонировании объекта мы используем присваивание, чтобы скопировать его свойства:
for (let key in user) {
clone[key] = user[key]
}
…Но это не копирует флаги. Так что если нам нужен клон «получше», предпочтительнее использовать Object.defineProperties
.
Другое отличие в том, что for..in
игнорирует символьные и неперечислимые свойства, а Object.getOwnPropertyDescriptors
возвращает дескрипторы всех свойств.
Глобальное запечатывание объекта
Дескрипторы свойств работают на уровне конкретных свойств.
Но ещё есть методы, которые ограничивают доступ ко всему объекту:
- Object.preventExtensions(obj)
- Запрещает добавлять новые свойства в объект.
- Object.seal(obj)
- Запрещает добавлять/удалять свойства. Устанавливает
configurable: false
для всех существующих свойств. - Object.freeze(obj)
- Запрещает добавлять/удалять/изменять свойства. Устанавливает
configurable: false, writable: false
для всех существующих свойств.
А также есть методы для их проверки:
- Object.isExtensible(obj)
- Возвращает
false
, если добавление свойств запрещено, иначеtrue
. - Object.isSealed(obj)
- Возвращает
true
, если добавление/удаление свойств запрещено и для всех существующих свойств установленоconfigurable: false
. - Object.isFrozen(obj)
- Возвращает
true
, если добавление/удаление/изменение свойств запрещено, и для всех текущих свойств установленоconfigurable: false, writable: false
.
На практике эти методы используются редко.