Пошук уроків, статей та іншого контенту
Керуватимете доступністю, змінністю та перелічуваністю властивостей через дескриптори.
Дескриптор — це об’єкт, який описує внутрішні характеристики властивості об’єкта JavaScript.
За допомогою дескрипторів можна керувати:
чи можна змінювати значення властивості;
чи можна видаляти або переналаштовувати властивість;
чи з’являється властивість під час переліку;
як властивість читається та змінюється через гетери й сетери.
На відміну від звичайного присвоєння:
const user = {};
user.name = "Olena";метод Object.defineProperty() дає змогу явно встановити всі необхідні параметри властивості.
У JavaScript існують два види дескрипторів:
дескриптор даних;
дескриптор доступу.
Дескриптор даних описує властивість, яка безпосередньо зберігає значення.
Він може містити такі поля:
value — значення властивості;
writable — чи можна змінити значення;
enumerable — чи буде властивість перелічуватися;
configurable — чи можна видалити або переналаштувати властивість.
const account = {};
Object.defineProperty(account, "balance", {
value: 1000,
writable: true,
enumerable: true,
configurable: true
});
console.log(account.balance); // 1000
account.balance = 1500;
console.log(account.balance); // 1500Дескриптор доступу не зберігає значення безпосередньо. Він визначає функції:
get — викликається під час читання властивості;
set — викликається під час запису;
enumerable;
configurable.
const user = {
firstName: "Olena",
lastName: "Koval"
};
Object.defineProperty(user, "fullName", {
enumerable: true,
configurable: true,
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
const [firstName, lastName] = value.split(" ");
this.firstName = firstName;
this.lastName = lastName;
}
});
console.log(user.fullName); // Olena Koval
user.fullName = "Ihor Bondar";
console.log(user.firstName); // Ihor
console.log(user.lastName); // BondarДескриптор не може одночасно містити поля дескриптора даних і дескриптора доступу.
Некоректний приклад:
const object = {};
Object.defineProperty(object, "value", {
value: 10,
get() {
return 10;
}
});Такий виклик завершується помилкою TypeError, оскільки властивість не може одночасно мати value і get.
Object.defineProperty()Синтаксис:
Object.defineProperty(object, propertyKey, descriptor);Наприклад:
const settings = {};
Object.defineProperty(settings, "theme", {
value: "dark",
writable: false,
enumerable: true,
configurable: false
});
console.log(settings.theme); // darkЯкщо поле не вказане в дескрипторі, для нової властивості використовуються значення за замовчуванням:
const object = {};
Object.defineProperty(object, "hidden", {
value: 42
});
console.log(Object.getOwnPropertyDescriptor(object, "hidden"));Результат:
{
value: 42,
writable: false,
enumerable: false,
configurable: false
}Це відрізняється від властивостей, створених через літерал об’єкта:
const object = {
visible: 42
};
console.log(Object.getOwnPropertyDescriptor(object, "visible"));Результат:
{
value: 42,
writable: true,
enumerable: true,
configurable: true
}Тому під час використання Object.defineProperty() важливо явно вказувати потрібні прапорці.
Object.getOwnPropertyDescriptor()Метод повертає дескриптор власної властивості об’єкта:
const product = {
name: "Keyboard",
price: 100
};
const descriptor = Object.getOwnPropertyDescriptor(product, "price");
console.log(descriptor);Результат:
{
value: 100,
writable: true,
enumerable: true,
configurable: true
}Якщо властивість не належить об’єкту безпосередньо, метод повертає undefined:
const parent = {
category: "hardware"
};
const product = Object.create(parent);
console.log(Object.getOwnPropertyDescriptor(product, "category"));
// undefinedВластивість успадкована від прототипу, тому вона не є власною властивістю product.
Object.getOwnPropertyDescriptors()Метод повертає дескриптори всіх власних властивостей:
const user = {
name: "Olena",
age: 28
};
const descriptors = Object.getOwnPropertyDescriptors(user);
console.log(descriptors);Це корисно, коли потрібно скопіювати об’єкт разом із його дескрипторами, а не лише значеннями.
Object.defineProperty()Object.defineProperty() може створити нову властивість або змінити дескриптор уже наявної.
const object = {
status: "draft"
};
Object.defineProperty(object, "status", {
writable: false,
enumerable: false
});
console.log(object.status); // draft
console.log(Object.keys(object)); // []Значення status залишилося незмінним, але властивість тепер:
не перелічується;
не допускає запису.
Прапорець, якого немає в новому дескрипторі, не скидається автоматично:
const object = {
count: 1
};
Object.defineProperty(object, "count", {
writable: false
});
console.log(Object.getOwnPropertyDescriptor(object, "count"));Результат:
{
value: 1,
writable: false,
enumerable: true,
configurable: true
}writablewritable визначає, чи можна змінити значення властивості.
"use strict";
const config = {};
Object.defineProperty(config, "apiUrl", {
value: "https://example.com",
writable: false,
enumerable: true,
configurable: true
});
config.apiUrl = "https://other.example.com";У строгому режимі спроба змінити властивість із writable: false викликає TypeError.
Без "use strict" така спроба зазвичай ігнорується:
const config = {};
Object.defineProperty(config, "apiUrl", {
value: "https://example.com",
writable: false
});
config.apiUrl = "https://other.example.com";
console.log(config.apiUrl); // https://example.comwritable: false не означає, що властивість неможливо видалити. За це відповідає інший прапорець — configurable.
enumerableenumerable визначає, чи враховується властивість під час переліку.
На нього впливають, зокрема:
Object.keys();
Object.values();
Object.entries();
for...in;
JSON.stringify().
const user = {
name: "Olena"
};
Object.defineProperty(user, "passwordHash", {
value: "hashed-value",
enumerable: false,
writable: false,
configurable: false
});
console.log(Object.keys(user)); // [ 'name' ]
console.log(user.passwordHash); // hashed-value
console.log(JSON.stringify(user)); // {"name":"Olena"}Неперелічувана властивість не стає приватною. До неї все ще можна звернутися за відомим ключем.
Для отримання всіх власних ключів, включно з неперелічуваними, використовують:
console.log(Object.getOwnPropertyNames(user));
// [ 'name', 'passwordHash' ]Для отримання також символьних ключів застосовують Reflect.ownKeys():
const identifier = Symbol("identifier");
const object = {
name: "record",
[identifier]: 123
};
Object.defineProperty(object, "hidden", {
value: true,
enumerable: false
});
console.log(Reflect.ownKeys(object));
// [ 'name', 'hidden', Symbol(identifier) ]configurableconfigurable визначає, чи можна:
видалити властивість;
змінити її дескриптор;
перетворити дескриптор даних на дескриптор доступу або навпаки.
const object = {};
Object.defineProperty(object, "temporary", {
value: 123,
configurable: true
});
delete object.temporary;
console.log("temporary" in object); // falseЯкщо configurable: false, видалення заборонене:
"use strict";
const object = {};
Object.defineProperty(object, "id", {
value: 10,
configurable: false
});
delete object.id;У строгому режимі це викличе TypeError.
Після встановлення configurable: false його не можна повернути до true:
const object = {};
Object.defineProperty(object, "id", {
value: 10,
configurable: false
});
Object.defineProperty(object, "id", {
configurable: true
});Це завершується помилкою.
Якщо властивість має configurable: false, можливості її подальшої зміни обмежені.
Наприклад, не можна змінити enumerable:
const object = {};
Object.defineProperty(object, "value", {
value: 1,
enumerable: true,
configurable: false
});
Object.defineProperty(object, "value", {
enumerable: false
});Також не можна замінити дескриптор даних на дескриптор доступу:
const object = {};
Object.defineProperty(object, "value", {
value: 1,
configurable: false
});
Object.defineProperty(object, "value", {
get() {
return 1;
}
});Окремий випадок — властивість із writable: true можна один раз зробити незаписуваною:
const object = {};
Object.defineProperty(object, "value", {
value: 1,
writable: true,
configurable: false
});
Object.defineProperty(object, "value", {
writable: false
});
console.log(object.value); // 1Після цього повернути writable: true вже не можна.
Гетер і сетер дають змогу контролювати читання та запис.
const temperature = {
_celsius: 20
};
Object.defineProperty(temperature, "celsius", {
enumerable: true,
configurable: true,
get() {
return this._celsius;
},
set(value) {
if (typeof value !== "number" || value < -273.15) {
throw new RangeError("Некоректне значення температури");
}
this._celsius = value;
}
});
console.log(temperature.celsius); // 20
temperature.celsius = 25;
console.log(temperature.celsius); // 25Гетер і сетер викликаються через звичайний синтаксис доступу:
temperature.celsius = 30; // викликається set
console.log(temperature.celsius); // викликається getНе потрібно викликати їх як звичайні функції.
Властивість можна створити лише з гетером:
const user = {
firstName: "Olena",
lastName: "Koval"
};
Object.defineProperty(user, "fullName", {
enumerable: true,
get() {
return `${this.firstName} ${this.lastName}`;
}
});
console.log(user.fullName); // Olena KovalУ такої властивості немає сетера. Запис у неї в строгому режимі викличе помилку.
"use strict";
user.fullName = "Ihor Bondar";Теоретично можна створити властивість лише із сетера:
const logger = {};
Object.defineProperty(logger, "message", {
set(value) {
console.log(`Отримано повідомлення: ${value}`);
}
});
logger.message = "Система запущена";
// Отримано повідомлення: Система запущена
console.log(logger.message); // undefinedТакий підхід використовується рідше, оскільки читання властивості не повертає збережене значення.
Object.defineProperties()Якщо потрібно створити кілька властивостей, можна використати Object.defineProperties():
const user = {};
Object.defineProperties(user, {
name: {
value: "Olena",
writable: true,
enumerable: true,
configurable: true
},
id: {
value: 101,
writable: false,
enumerable: true,
configurable: false
},
displayName: {
enumerable: true,
configurable: true,
get() {
return `${this.name} (#${this.id})`;
}
}
});
console.log(user.displayName); // Olena (#101)Метод повертає сам об’єкт, тому його можна використовувати під час створення та налаштування об’єктів.
Звичайне копіювання через розширення об’єкта або Object.assign() копіює значення, але не дескриптори:
const source = {};
Object.defineProperty(source, "secret", {
value: 42,
enumerable: false,
writable: false,
configurable: false
});
const copyWithValues = Object.assign({}, source);
console.log(Object.getOwnPropertyDescriptor(copyWithValues, "secret"));
// undefinedНеперелічувана властивість взагалі не була скопійована.
Для копіювання всіх власних властивостей разом із дескрипторами використовують:
const source = {};
Object.defineProperty(source, "secret", {
value: 42,
enumerable: false,
writable: false,
configurable: false
});
const copy = Object.create(
Object.getPrototypeOf(source),
Object.getOwnPropertyDescriptors(source)
);
console.log(copy.secret); // 42
console.log(Object.getOwnPropertyDescriptor(copy, "secret"));Такий підхід зберігає:
неперелічувані властивості;
гетери та сетери;
значення writable;
значення configurable;
прототип об’єкта.
Дескриптор властивості перевіряється спочатку на самому об’єкті, а потім у його прототипному ланцюжку.
const prototype = {};
Object.defineProperty(prototype, "id", {
value: 100,
writable: false,
enumerable: true,
configurable: true
});
const object = Object.create(prototype);
console.log(object.id); // 100id не є власною властивістю object:
console.log(Object.hasOwn(object, "id")); // falseЯкщо успадкована властивість є незаписуваною властивістю даних, просте присвоєння не змінить її значення через дочірній об’єкт:
"use strict";
const prototype = {};
Object.defineProperty(prototype, "id", {
value: 100,
writable: false
});
const object = Object.create(prototype);
object.id = 200;У строгому режимі це викличе TypeError.
Натомість успадкований сетер може бути викликаний під час запису:
const prototype = {
set value(newValue) {
this._value = newValue;
}
};
const object = Object.create(prototype);
object.value = 50;
console.log(object._value); // 50Сетер отримує this, що вказує на об’єкт, через який виконано запис, тобто на object, а не на prototype.
Object.freeze()Object.freeze() робить власні властивості об’єкта незмінюваними на рівні структури.
Для звичайних властивостей це означає:
writable: false;
configurable: false.
Перелічуваність зазвичай не змінюється.
const settings = {
theme: "dark"
};
Object.freeze(settings);
console.log(Object.getOwnPropertyDescriptor(settings, "theme"));
// {
// value: "dark",
// writable: false,
// enumerable: true,
// configurable: false
// }Object.freeze() є поверхневим:
const state = {
user: {
name: "Olena"
}
};
Object.freeze(state);
state.user.name = "Ihor";
console.log(state.user.name); // IhorВнутрішній об’єкт не був заморожений автоматично.
Object.seal()Object.seal():
забороняє додавання властивостей;
забороняє видалення властивостей;
встановлює configurable: false;
не вимикає writable.
const account = {
balance: 100
};
Object.seal(account);
account.balance = 200;
console.log(account.balance); // 200
console.log(Object.getOwnPropertyDescriptor(account, "balance"));
// {
// value: 200,
// writable: true,
// enumerable: true,
// configurable: false
// }Тому seal і freeze мають різний рівень обмежень:
seal залишає значення властивостей змінюваними;
freeze забороняє змінювати значення звичайних властивостей.
Гетери можуть обчислювати значення ліниво та кешувати результат:
"use strict";
function createReport(items) {
let totalCalculations = 0;
const report = {
items,
get total() {
totalCalculations += 1;
return items.reduce((sum, item) => sum + item.price, 0);
},
get calculations() {
return totalCalculations;
}
};
return report;
}
const report = createReport([
{ name: "Keyboard", price: 100 },
{ name: "Mouse", price: 50 }
]);
console.log(report.total); // 150
console.log(report.calculations); // 1
console.log(report.total); // 150
console.log(report.calculations); // 2У цьому прикладі значення обчислюється під час кожного читання. Для кешування можна використати Object.defineProperty():
"use strict";
function createCachedReport(items) {
const report = { items };
Object.defineProperty(report, "total", {
enumerable: true,
configurable: false,
get() {
const total = items.reduce((sum, item) => sum + item.price, 0);
Object.defineProperty(report, "total", {
value: total,
enumerable: true,
configurable: false,
writable: false
});
return total;
}
});
return report;
}
const report = createCachedReport([
{ name: "Keyboard", price: 100 },
{ name: "Mouse", price: 50 }
]);
console.log(report.total); // 150
console.log(Object.getOwnPropertyDescriptor(report, "total"));Під час першого читання гетер замінює себе на звичайну властивість даних. Наступні читання отримують уже збережене значення.
Такий підхід корисний, коли:
обчислення дороге;
результат не змінюється;
значення потрібне не завжди.
ProxyОб’єкт Proxy може перехоплювати операції з властивостями. Зокрема, пастка getOwnPropertyDescriptor може змінювати результат отримання дескриптора.
Однак Proxy зобов’язаний дотримуватися інваріантів JavaScript. Він не може повідомити дескриптор, який суперечить незмінюваним властивостям цільового об’єкта.
const target = {};
Object.defineProperty(target, "id", {
value: 1,
writable: false,
enumerable: true,
configurable: false
});
const proxy = new Proxy(target, {
getOwnPropertyDescriptor(targetObject, property) {
const descriptor = Reflect.getOwnPropertyDescriptor(
targetObject,
property
);
if (!descriptor) {
return undefined;
}
return {
...descriptor,
enumerable: false
};
}
});
console.log(Object.getOwnPropertyDescriptor(proxy, "id"));Такий код викличе TypeError, оскільки властивість id є неконфігурованою, а проксі намагається повернути несумісний дескриптор.
Для коректної роботи з дескрипторами в проксі зазвичай використовують Reflect:
const target = {
id: 1
};
const proxy = new Proxy(target, {
getOwnPropertyDescriptor(targetObject, property) {
return Reflect.getOwnPropertyDescriptor(targetObject, property);
}
});
console.log(Object.getOwnPropertyDescriptor(proxy, "id"));Під час зміни наявної властивості відсутні поля дескриптора не скидаються:
const object = {
value: 10
};
Object.defineProperty(object, "value", {
writable: false
});Це змінює лише writable, а enumerable і configurable залишаються такими, якими були.
Для нової властивості ситуація інша: пропущені поля отримують значення false або undefined.
writable і configurableconst object = {};
Object.defineProperty(object, "value", {
value: 10,
writable: false,
configurable: true
});Тут не можна змінити value через присвоєння, але можна змінити дескриптор або видалити властивість.
І навпаки:
const object = {};
Object.defineProperty(object, "value", {
value: 10,
writable: true,
configurable: false
});Значення можна змінювати, але властивість не можна видалити або повністю переналаштувати.
value і getДескриптор має бути або дескриптором даних, або дескриптором доступу:
Object.defineProperty(object, "value", {
value: 10,
get() {
return 10;
}
});Потрібно вибрати один із підходів.
enumerable: false робить властивість приватноюНеперелічувану властивість можна прочитати напряму:
console.log(object.hidden);enumerable лише керує участю властивості в операціях переліку.
Object.assign() для копіювання гетерівObject.assign() читає значення властивостей джерела, а не копіює їхні дескриптори. У результаті гетер може бути викликаний, а в новому об’єкті з’явиться звичайна властивість.
Для збереження гетерів і сетерів використовуйте Object.getOwnPropertyDescriptors() разом із Object.defineProperties() або Object.create().
У нестрогому режимі деякі заборонені операції мовчки ігноруються. У строгому режимі вони викликають помилки.
Під час розроблення API або бібліотек корисно тестувати поведінку в строгому режимі:
"use strict";Дескриптор описує поведінку власної властивості об’єкта.
Дескриптор даних використовує value і writable.
Дескриптор доступу використовує get і set.
enumerable керує участю властивості в операціях переліку.
writable визначає, чи можна змінювати значення.
configurable визначає, чи можна видаляти або переналаштовувати властивість.
Object.defineProperty() створює або змінює одну властивість.
Object.defineProperties() працює з кількома властивостями.
Object.getOwnPropertyDescriptor() повертає дескриптор однієї властивості.
Object.getOwnPropertyDescriptors() повертає дескриптори всіх власних властивостей.
Гетери та сетери дають змогу реалізувати обчислювані, перевірювані та контрольовані властивості.
Object.assign() копіює значення, а не дескриптори.
Object.freeze() і Object.seal() змінюють дескриптори властивостей та обмежують подальші операції.