Property descriptor (дескриптор властивості)
Property descriptor (дескриптор властивості) це внутрішній об'єкт, який описує поведінку властивості об'єкта: чи можна її змінювати, перелічувати, видаляти і чи має вона гетер або сетер. Простіше кажучи, це метадані про властивість: не лише її значення, а й прапорці, що визначають, як із нею можна працювати.
Теорія
TL;DR
- Дескриптор описує не значення, а правила роботи з властивістю.
- Властивості бувають двох типів: data property (
value,writable) і accessor property (get,set). - Спільні прапорці обох типів:
enumerable(чи видно при переборі) іconfigurable(чи можна видалити або переналаштувати). - Читання:
Object.getOwnPropertyDescriptorіObject.getOwnPropertyDescriptors. - Запис:
Object.definePropertyіObject.defineProperties. - У звичайної властивості всі прапорці
true, у створеної черезdefinePropertyпропущені прапорці стаютьfalse.
Швидкий приклад
const user = { name: 'Tim' };
const descriptor = Object.getOwnPropertyDescriptor(user, 'name');
console.log(descriptor);
// {
// value: 'Tim',
// writable: true,
// enumerable: true,
// configurable: true
// }Усі ці прапорці за замовчуванням дорівнюють true, якщо властивість створено звичайним способом.
Види властивостей і структура дескриптора
Властивості бувають двох типів:
- Data property (властивість-дані) зберігає конкретне значення у полі
value. - Accessor property (властивість-аксесор) визначає функції
getіset.
Для властивості-даних:
{
value: any, // значення
writable: boolean, // чи можна змінювати value
enumerable: boolean, // чи видно при переборі (for...in, Object.keys)
configurable: boolean // чи можна видалити або змінити дескриптор
}Для властивості-аксесора:
{
get: function | undefined, // гетер
set: function | undefined, // сетер
enumerable: boolean,
configurable: boolean
}Один дескриптор не може одночасно мати value або writable і get або set: це або дані, або аксесор.
Як задати властивість вручну
const user = {};
Object.defineProperty(user, 'role', {
value: 'admin',
writable: false, // не можна змінювати значення
enumerable: false, // не з'являється при переборі
configurable: false // не можна видалити або переналаштувати
});
console.log(user.role); // 'admin'
user.role = 'user';
console.log(user.role); // 'admin', значення не змінилосяПеревіримо перелічуваність і конфігурованість:
const user = {};
Object.defineProperty(user, 'name', {
value: 'Tim',
enumerable: false,
configurable: false
});
console.log(Object.keys(user)); // [], не перелічується
delete user.name; // не видалиться
console.log(user.name); // 'Tim'У нестрогому режимі такі спроби мовчки ігноруються, у 'use strict' вони кидають TypeError.
Масове визначення властивостей:
const user = {};
Object.defineProperties(user, {
name: {
value: 'Alex',
writable: true,
enumerable: true
},
age: {
value: 25,
writable: false
}
});Отримати всі дескриптори об'єкта одразу:
const obj = { a: 1, b: 2 };
console.log(Object.getOwnPropertyDescriptors(obj));
// {
// a: { value: 1, writable: true, enumerable: true, configurable: true },
// b: { value: 2, writable: true, enumerable: true, configurable: true }
// }Гетер і сетер у дескрипторі
const person = {};
let _age = 25;
Object.defineProperty(person, 'age', {
get() {
return _age;
},
set(value) {
if (value < 0) throw new Error('Вік не може бути від'ємним');
_age = value;
},
enumerable: true,
configurable: true
});
console.log(person.age); // 25
person.age = 30; // викликає setter
console.log(person.age); // 30Ззовні person.age виглядає як звичайна властивість, але кожне читання і запис проходять через ваші функції, і саме тут зручно робити валідацію.
Застосування в реальних задачах
- Створення властивостей «тільки для читання»:
Object.defineProperty(config, 'API_KEY', {
value: '12345',
writable: false
});- Приховані властивості, які не потрапляють у перебір і в
JSON.stringify:
Object.defineProperty(obj, '_id', {
value: 42,
enumerable: false
});-
Реалізація реактивності: Vue 2, наприклад, використовував
getіsetу дескрипторах, щоб відстежувати зміни даних. -
Імітація приватних полів (до появи синтаксису
#):
Object.defineProperty(obj, 'secret', {
value: 'hidden',
enumerable: false
});Підсумкова таблиця полів:
| Поле дескриптора | Для чого потрібне |
|---|---|
value | саме значення |
writable | чи можна змінити value |
enumerable | чи видно при переборі |
configurable | чи можна видалити або перевизначити |
get | функція-гетер |
set | функція-сетер |
Типові помилки
- Очікувати, що пропущені прапорці будуть
true. УObject.definePropertyусе, чого ви не вказали, дорівнюєfalse, тож властивість несподівано стає нередагованою і неперелічуваною. - Плутати
writable: falseіз глибокою незмінністю. Замкнути можна лише саме посилання: поля об'єкта, на який воно вказує, і далі змінюються. - Комбінувати
valueзgetабоsetв одному дескрипторі. Це одразуTypeError, властивість може бути або даними, або аксесором. - Не помічати тихого провалу. Без
'use strict'запис уwritable: falseвластивість нічого не робить і не кидає помилки, що виглядає як загадковий баг. - Ставити
configurable: falseзавчасно. Назад дороги немає: переналаштувати або видалити таку властивість уже не вийде. - Розраховувати, що
Object.assignабо spread перенесуть дескриптори. Вони копіюють лише значення власних перелічуваних властивостей, гетери при цьому виконуються. Щоб зберегти дескриптори, використовуйтеObject.defineProperties(target, Object.getOwnPropertyDescriptors(source)). - Забувати, що
enumerable: falseховає властивість відObject.keys,for...inіJSON.stringify, але не робить її приватною:Object.getOwnPropertyNamesїї покаже.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.