Пошук уроків, статей та іншого контенту
Налаштуєте доступ, запис і перелік властивостей через дескриптори та методи Object.defineProperty.
Дескриптор властивості — це об’єкт, який описує поведінку властивості іншого об’єкта:
чи можна читати її значення;
чи можна змінювати значення;
чи потрапляє вона до переліку властивостей;
чи можна видалити або переналаштувати її;
як виконуються читання та запис для властивостей-аксесорів.
Отримати дескриптор уже наявної властивості можна за допомогою Object.getOwnPropertyDescriptor():
const user = {
name: "Olena",
};
const descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// {
// value: "Olena",
// writable: true,
// enumerable: true,
// configurable: true
// }Метод повертає дескриптор лише власної властивості об’єкта. Властивості, успадковані через прототип, він не знаходить.
Дескриптор даних описує властивість, яка зберігає значення безпосередньо.
Він може містити такі поля:
value — значення властивості;
writable — чи можна змінювати значення;
enumerable — чи буде властивість видимою під час переліку;
configurable — чи можна видалити властивість або змінити її дескриптор.
Наприклад:
const settings = {};
Object.defineProperty(settings, "version", {
value: "1.0.0",
writable: false,
enumerable: true,
configurable: false,
});
console.log(settings.version); // "1.0.0"
settings.version = "2.0.0";
console.log(settings.version); // "1.0.0"Властивість version доступна для читання, але її значення не можна змінити.
Під час створення властивості через Object.defineProperty() усі поля, які не вказані явно, мають значення за замовчуванням:
value — undefined;
writable — false;
enumerable — false;
configurable — false.
Тому цей код створює властивість, яку не можна змінювати, яка не перелічується та не переналаштовується:
const object = {};
Object.defineProperty(object, "secret", {
value: "hidden",
});
console.log(object.secret); // "hidden"
console.log(Object.keys(object)); // []Це відрізняється від звичайного створення властивості:
const object = {};
object.secret = "hidden";
console.log(Object.getOwnPropertyDescriptor(object, "secret"));
// {
// value: "hidden",
// writable: true,
// enumerable: true,
// configurable: true
// }Object.defineProperty()Метод Object.defineProperty() створює нову властивість або змінює дескриптор уже наявної:
Object.defineProperty(object, propertyName, descriptor);Приклад:
const product = {};
Object.defineProperty(product, "id", {
value: 101,
writable: false,
enumerable: true,
configurable: false,
});
console.log(product.id); // 101Якщо властивість уже існує, метод може змінити її налаштування, якщо це дозволяє configurable.
const user = {
name: "Andrii",
};
Object.defineProperty(user, "name", {
enumerable: false,
});
console.log(user.name); // "Andrii"
console.log(Object.keys(user)); // []Значення властивості залишилося без змін, але вона більше не перелічується.
Object.defineProperty() повертає сам об’єкт:
const object = Object.defineProperty({}, "status", {
value: "active",
});
console.log(object.status); // "active"Це дає змогу створювати об’єкт і налаштовувати його в одному виразі.
writable, enumerable і configurablewritableВизначає, чи можна змінити значення властивості:
"use strict";
const account = {};
Object.defineProperty(account, "balance", {
value: 1000,
writable: false,
enumerable: true,
});
account.balance = 2000;У строгому режимі спроба змінити властивість із writable: false спричинить TypeError.
Без строгого режиму така операція зазвичай ігнорується, що може ускладнити пошук помилки. Для передбачуваної поведінки під час розробки корисно використовувати строгий режим або модулі, які працюють у строгому режимі.
enumerableВизначає, чи буде властивість доступною під час переліку.
На enumerable: true зважають, зокрема:
Object.keys();
Object.values();
Object.entries();
for...in;
JSON.stringify().
const user = {
name: "Marta",
};
Object.defineProperty(user, "password", {
value: "qwerty",
enumerable: false,
});
console.log(Object.keys(user)); // ["name"]
console.log(user.password); // "qwerty"
console.log(JSON.stringify(user)); // {"name":"Marta"}Прихована від переліку властивість не є справді приватною. До неї все одно можна звернутися безпосередньо, якщо відоме її ім’я.
configurableВизначає, чи можна:
видалити властивість;
змінити її дескриптор;
перетворити дескриптор даних на дескриптор-аксесор і навпаки.
const object = {};
Object.defineProperty(object, "id", {
value: 10,
configurable: true,
});
delete object.id;
console.log(object.id); // undefinedЯкщо configurable: false, видалити властивість не можна:
"use strict";
const object = {};
Object.defineProperty(object, "id", {
value: 10,
configurable: false,
});
delete object.id; // TypeError у строгому режиміПісля встановлення configurable: false його не можна змінити на true.
Аксесорна властивість не зберігає значення безпосередньо через поле value. Вона використовує функції:
get — виконується під час читання;
set — виконується під час запису.
Приклад:
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 Melnyk";
console.log(user.firstName); // "Ihor"
console.log(user.lastName); // "Melnyk"
console.log(user.fullName); // "Ihor Melnyk"У дескрипторі-аксесорі не можна одночасно використовувати value або writable разом із get чи set.
Неправильно:
const object = {};
Object.defineProperty(object, "value", {
value: 10,
get() {
return 20;
},
});Для однієї властивості потрібно вибрати один тип дескриптора:
дескриптор даних: value, writable;
дескриптор-аксесор: get, set.
Якщо вказати тільки get, властивість матиме доступ для читання, але не матиме власного запису:
const rectangle = {
width: 10,
height: 5,
};
Object.defineProperty(rectangle, "area", {
enumerable: true,
get() {
return this.width * this.height;
},
});
console.log(rectangle.area); // 50Властивість area обчислюється щоразу під час читання:
rectangle.width = 20;
console.log(rectangle.area); // 100Дескриптор можна отримати, змінити та застосувати знову:
const article = {
title: "JavaScript",
};
const descriptor = Object.getOwnPropertyDescriptor(article, "title");
Object.defineProperty(article, "title", {
...descriptor,
writable: false,
});
article.title = "New title";
console.log(article.title); // "JavaScript"Розгортання ...descriptor важливе, якщо потрібно зберегти інші налаштування. Якщо передати лише:
Object.defineProperty(article, "title", {
writable: false,
});то для полів, які не вказані в новому дескрипторі, діють правила defineProperty, а не поточні значення властивості.
Для одночасного створення кількох властивостей існує Object.defineProperties():
const book = {};
Object.defineProperties(book, {
title: {
value: "The Pragmatic Programmer",
enumerable: true,
writable: false,
configurable: false,
},
pages: {
value: 352,
enumerable: true,
writable: true,
configurable: true,
},
});
console.log(book.title); // "The Pragmatic Programmer"
console.log(book.pages); // 352
console.log(Object.keys(book)); // ["title", "pages"]Цей метод приймає:
об’єкт, який потрібно змінити;
об’єкт із назвами властивостей і їхніми дескрипторами.
Дескриптори-аксесори корисні, коли потрібно перевіряти дані під час запису або приховувати внутрішній стан.
function createUser(name, age) {
let internalAge = age;
const user = {
name,
};
Object.defineProperty(user, "age", {
enumerable: true,
configurable: false,
get() {
return internalAge;
},
set(value) {
if (!Number.isInteger(value) || value < 0) {
throw new TypeError("Вік має бути невід'ємним цілим числом");
}
internalAge = value;
},
});
return user;
}
const user = createUser("Sofiia", 25);
console.log(user.name); // "Sofiia"
console.log(user.age); // 25
user.age = 26;
console.log(user.age); // 26
try {
user.age = -1;
} catch (error) {
console.log(error.message); // Вік має бути невід'ємним цілим числом
}
console.log(Object.keys(user)); // ["name", "age"]Змінна internalAge недоступна безпосередньо ззовні. Вона доступна лише функціям get і set, створеним усередині createUser().
Для аналізу властивостей можна використовувати кілька методів:
const object = {
visible: "yes",
};
Object.defineProperty(object, "hidden", {
value: "no",
enumerable: false,
});
console.log(Object.getOwnPropertyDescriptor(object, "visible"));
console.log(Object.getOwnPropertyDescriptor(object, "hidden"));
console.log(Object.getOwnPropertyDescriptors(object));Object.getOwnPropertyDescriptors() повертає дескриптори всіх власних властивостей:
console.log(Object.getOwnPropertyDescriptors(object));
// {
// visible: {
// value: "yes",
// writable: true,
// enumerable: true,
// configurable: true
// },
// hidden: {
// value: "no",
// writable: false,
// enumerable: false,
// configurable: false
// }
// }Це може бути корисним під час клонування об’єктів зі збереженням не лише значень, а й поведінки властивостей:
const clone = Object.create(
Object.getPrototypeOf(object),
Object.getOwnPropertyDescriptors(object),
);
console.log(clone.visible); // "yes"
console.log(clone.hidden); // "no"
console.log(Object.keys(clone)); // ["visible"]Звичайний spread або Object.assign() копіюють значення властивостей, але не копіюють їхні дескриптори:
const copy = { ...object };
console.log(Object.getOwnPropertyDescriptor(copy, "hidden"));
// undefinedObject.freeze() та Object.seal()Методи Object.freeze() і Object.seal() також змінюють властивості через їхні дескриптори.
Object.seal()Object.seal():
забороняє додавати нові властивості;
забороняє видаляти наявні властивості;
робить наявні властивості неконфігурованими;
не забороняє змінювати значення, якщо властивості залишаються доступними для запису.
Object.freeze()Object.freeze() додатково робить властивості даних недоступними для запису.
const settings = {
theme: "light",
};
Object.freeze(settings);
settings.theme = "dark";
console.log(settings.theme); // "light"Заморожування є поверхневим: вкладені об’єкти не заморожуються автоматично.
const state = {
user: {
name: "Oleh",
},
};
Object.freeze(state);
state.user.name = "Petro";
console.log(state.user.name); // "Petro"Щоб повністю заморозити вкладену структуру, потрібно рекурсивно заморозити кожен об’єкт.
defineProperty() збереже звичайні значення за замовчуваннямconst object = {};
Object.defineProperty(object, "count", {
value: 0,
});
object.count = 1;
console.log(object.count); // 0Якщо властивість має поводитися як звичайна змінювана властивість, потрібно явно вказати:
Object.defineProperty(object, "count", {
value: 0,
writable: true,
enumerable: true,
configurable: true,
});enumerable: false лише прибирає властивість із деяких способів переліку. Вона не захищає дані:
const user = {};
Object.defineProperty(user, "token", {
value: "secret-token",
enumerable: false,
});
console.log(user.token); // "secret-token"Для справді приватного стану можна використовувати замикання або приватні поля класу з префіксом #.
Не можна використовувати value і get для однієї властивості. Так само не можна поєднувати writable із set.
Якщо в дескрипторі не вказати enumerable: true, властивість не з’явиться в Object.keys(), for...in та JSON-серіалізації.
Якщо configurable уже має значення false, можливості переналаштування обмежені. Тому властивості, які можуть потребувати змін у майбутньому, не варто без необхідності робити неконфігурованими.
Дескриптор описує поведінку властивості об’єкта.
Дескриптори даних використовують value і writable.
Дескриптори-аксесори використовують get і set.
writable керує можливістю змінювати значення.
enumerable керує участю властивості в переліку та JSON-серіалізації.
configurable керує видаленням і переналаштуванням властивості.
Object.defineProperty() створює або змінює одну властивість.
Object.defineProperties() дає змогу налаштувати кілька властивостей.
Object.getOwnPropertyDescriptor() повертає дескриптор однієї властивості.
Object.getOwnPropertyDescriptors() повертає дескриптори всіх власних властивостей.
Прихована через enumerable: false властивість не є приватною.
Для однієї властивості потрібно вибрати або дескриптор даних, або дескриптор-аксесор.