Як відомо, об’єкти можуть зберігати властивості.
Дотепер, для нас властивість була простою парою “ключ-значення”. Однак насправді, властивість об’єкта є гнучкішою та потужнішою.
У цьому розділі ми вивчимо додаткові параметри конфігурації, а в наступному ми побачимо, як можна непомітно перетворити їх у функції – гетери та сетери.
Прапори властивостей
Властивості об’єкта, крім value
, мають три спеціальні атрибути (так звані “прапори”):
writable
– якщоtrue
, значення можна змінювати, інакше воно доступне лише для читання.enumerable
– якщоtrue
, то властивість враховується в циклах, в іншому випадку цикли його ігнорують.configurable
– якщоtrue
, властивість можна видалити, а ці атрибути можна змінювати, інакше цього робити не можна.
Ми ще не бачили цих атрибутів, тому що вони зазвичай не показуються. Коли ми створюємо властивість “звичайним способом”, всі атрибути мають значення true
. Але ми також можемо їх змінити в будь-який час.
По-перше, подивімось, як отримати ці прапори.
Метод Object.getOwnPropertyDescriptor дозволяє отримати повну інформацію про властивість.
Синтаксис:
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
obj
- Об’єкт, з якого буде отримана інформація.
propertyName
- Назва властивості.
Повернене значення – це так званий об’єкт “дескриптор властивості”: він містить значення та всі прапори.
Наприклад:
let user = {
name: "Іван"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
"value": "Іван",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
Щоб змінити прапори, ми можемо використовувати Object.defineProperty.
Синтаксис:
Object.defineProperty(obj, propertyName, descriptor)
obj
,propertyName
- Об’єкт і його властивість, щоб застосувати дескриптор.
descriptor
- Об’єкт дескриптора властивості, який буде застосовано.
Якщо властивість існує, defineProperty
оновлює її прапори. В іншому випадку метод створює властивість з даним значенням та прапорами; у цьому випадку, якщо прапор не вказано, передбачається, що його значення false
.
Наприклад, тут властивість name
створюється з усіма хибними прапорами:
let user = {};
Object.defineProperty(user, "name", {
value: "Іван"
});
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "Іван",
"writable": false,
"enumerable": false,
"configurable": false
}
*/
Порівняйте це з попереднім прикладом “нормального створення” user.name
вище: тепер всі прапори є хибними. Якщо це не те, що ми хочемо, тоді ми краще встановили їх значення в true
у descriptor
.
Тепер розгляньмо ефекти від прапорів на прикладах.
Не для запису
Зробімо user.name
недоступним для запису (недоступним для переприсвоєння), змінюючи writable
прапор:
let user = {
name: "Іван"
};
Object.defineProperty(user, "name", {
writable: false
});
user.name = "Петро"; // Помилка: неможливо присвоїти доступну лише для читання властивість 'name'
Тепер ніхто не може змінити ім’я нашого користувача, якщо тільки не оновить відповідний прапор викликавши defineProperty
, щоб перекрити наші дескриптори.
В несуворому режимі (без use strict
), під час запису значення до недоступних для запису властивостей не виникне помилки. Однак така операція все одно не змінить значення. Дії, що порушують прапори, просто мовчки ігноруються в несуворому режимі.
Ось той же приклад, але властивість створена “з нуля”:
let user = { };
Object.defineProperty(user, "name", {
value: "Іван",
// для нових властивостей ми повинні явно вказати всі прапори, для яких значення true
enumerable: true,
configurable: true
});
alert(user.name); // Іван
user.name = "Петро"; // Помилка
Неперелічувана властивість
Тепер додаймо кастомний toString
до user
.
Зазвичай, вбудований toString
для об’єктів неперелічуваний, тобто він не з’являється в for..in
. Але якщо ми додамо свій власний toString
, то за замовчуванням він з’являється в for..in
:
let user = {
name: "Іван",
toString() {
return this.name;
}
};
// за замовчуванням, вказані обидві наші властивості:
for (let key in user) alert(key); // name, toString
Якщо нам це не подобається, то ми можемо встановити enumerable:false
. Тоді toString
не з’явиться в for..in
так само, як вбудований метод.
let user = {
name: "Іван",
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
// видалення Math.PI також не буде працювати
Ми також не можемо змінити властивість Math.PI
, щоб вона знову була writable
:
// Помилка, через configurable: false
Object.defineProperty(Math, "PI", { writable: true });
Абсолютно нічого не можна зробити з Math.PI
.
Створення неналаштовуваної властивості – це дорога в один кінець. Ми не можемо змінити цю властивість з defineProperty
.
Зверніть увагу: configurable: false
не дозволяє зміну чи видалення прапорів властивості, однак дозволяє змінювати значення властивості.
Тут user.name
неналаштовувана властивість, але ми все ще можемо змінити її (бо вона доступна для запису):
let user = {
name: "Іван"
};
Object.defineProperty(user, "name", {
configurable: false
});
user.name = "Петро"; // працює добре
delete user.name; // Помилка
І ось ми робимо user.name
“назавжди запечатаною” константою, як і вбудована Math.PI
:
let user = {
name: "Іван"
};
Object.defineProperty(user, "name", {
writable: false,
configurable: false
});
// тепер не можливо змінювати user.name чи її прапори
// все це не буде працювати:
user.name = "Pete";
delete user.name;
Object.defineProperty(user, "name", { value: "Петро" });
Існує незначний виняток щодо зміни прапорів.
Ми можемо змінити writable: true
на false
для не неналаштовуваної властивості, таким чином, запобігаючи модифікації її значення (додає інший шар захисту). Навпаки такий підхід не працює.
Object.defineProperties
Існує метод Object.defineProperties(obj, descriptors), що дозволяє визначити багато властивостей відразу.
Синтаксис:
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
Наприклад:
Object.defineProperties(user, {
name: { value: "Іван", writable: false },
surname: { value: "Іванов", 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
.
Ці методи рідко використовуються на практиці.