24 квітня 2022 р.

Прапори та дескриптори властивостей

Як відомо, об’єкти можуть зберігати властивості.

Дотепер, для нас властивість була простою парою “ключ-значення”. Однак насправді, властивість об’єкта є гнучкішою та потужнішою.

У цьому розділі ми вивчимо додаткові параметри конфігурації, а в наступному ми побачимо, як можна непомітно перетворити їх у функції – гетери та сетери.

Прапори властивостей

Властивості об’єкта, крім 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

Існує незначний виняток щодо зміни прапорів.

Ми можемо змінити 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.

Ці методи рідко використовуються на практиці.

Навчальна карта