Object.defineProperty()
Статичний метод Object.defineProperty()
("означити властивість") визначає нову властивість безпосередньо на об'єкті, або змінює вже наявну властивість на об'єкті, і повертає цей об'єкт.
Спробуйте його в дії
Синтаксис
Object.defineProperty(obj, prop, descriptor)
Параметри
obj
Об'єкт, на якому буде визначено передану властивість.
prop
Назва або
символ
властивості, яку буде створено або модифіковано.descriptor
Дескриптор властивості, яка визначається чи модифікується.
Повернене значення
Об'єкт, який було передано до функції.
Опис
Цей метод дає можливість делікатно додавати чи модифікувати властивість об'єкта. Звичайне додавання властивостей шляхом присвоєння породжує властивості, які потім з'являються під час перелічування властивостей (цикл for...in
або метод Object.keys
), чиї значення можна змінювати, і які можна видаляти. Цей метод дає змогу змінити ці додаткові деталі від їхніх усталених значень. Типово, додані методом Object.defineProperty()
властивості не є ні записуваними, ні перелічуваними, ні налаштовними.
Наявні в об'єктах дескриптори властивостей поділяються на два основні види: дескриптори даних, та аксесорні дескриптори. Дескриптор даних — це властивість, що має значення, яке може або бути записуваним, або ні. Аксесорний дескриптор — це властивість, описана парою функцій з гетера й сетера. Будь-який дескриптор мусить належати до одного з цих двох видів, не обох.
Що дескриптор даних, що аксесорний дескриптор — це об'єкти. Вони поділяють наведений далі набір необов'язкових ключів (слід зауважити, що згадані тут усталені значення стосуються означення властивостей за допомогою Object.defineProperty()
):
configurable
(налаштовний)в разі, якщо містить значення
false
:- тип цієї властивості не можна змінити від властивості даних до аксесорної властивості, і навпаки;
- цю властивість не можна видалити;
- інші атрибути дескриптора цієї властивості не можна змінити (проте, якщо цей дескриптор має
writable: true
, значення йогоvalue
міняти можна, як і змінювати атрибутwritable
наfalse
).
Усталено дорівнює
false
.enumerable
(перелічуваний)дорівнює
true
лише в тому разі, якщо ця властивість з'являється під час перелічування властивостей відповідного об'єкта. Усталено дорівнюєfalse
.
Дескриптор даних також може містити наступні необов'язкові ключі:
value
(значення)Власне значення, яке асоційовано з даною властивістю. Може бути будь-яким дійсним в JavaScript значенням (числом, об'єкт, функція тощо). Усталено дорівнює
undefined
.writable
(записуваний)Дорівнює
true
в тому разі, якщо властивість можна змінювати шляхом застосування оператора присвоєння. Усталено дорівнюєfalse
.
Аксесорний дескриптор також має наступні необов'язкові ключі:
get
(взяти)Функція, яка слугує гетером для цієї властивості, або
undefined
— якщо гетера немає. Під час виконання доступу до властивості, ця функція викликається без аргументів, причому її значеннямthis
встановлюється об'єкт, через який відбувається доступ до цієї властивості (у зв'язку з успадкуванням, це може бути не той об'єкт, на якому цю властивість було означено). Повернене з функції значення буде використано як значення властивості. Усталено дорівнюєundefined
.set
(поставити)Функція, яка слугує сетером для цієї властивості, або
undefined
— якщо сетера немає. Під час присвоєння властивості значення, ця функція викликається з одним аргументом (значенням, яке присвоюється цій властивості), причому її значеннямthis
встановлюється об'єкт, через який відбувається присвоєння цієї властивості. Усталено дорівнюєundefined
.
Якщо дескриптор не містить жодного ключа з-поміж value
, writable
, get
і set
, він вважається дескриптором даних. Якщо дескриптор одночасно містить і ключ [value
чи writable
], і [get
чи set
] — викидається виняток.
Слід мати на увазі, що ці атрибути не зобов'язані бути власними властивостями дескриптора: успадковані властивості також будуть враховуватися. Аби впевнитися, що ці усталені значення будуть збережені без змін, можна заздалегідь заморозити об'єкти, наявні в прототипному ланцюжку дескрипторного об'єкта, вказати всі атрибути явно, чи створити null
-прототипний об'єкт.
const obj = {};
// 1. Застосування null як прототипа – немає успадкованих властивостей
const descriptor = Object.create(null);
descriptor.value = "static";
// як усталено: неперелічувана, неналаштовна, незаписувана
Object.defineProperty(obj, "key", descriptor);
// 2. Явним чином, шляхом використання одноразового об'єктного літерала
// із вказаними всіма атрибутами
Object.defineProperty(obj, "key2", {
enumerable: false,
configurable: false,
writable: false,
value: "static",
});
// 3. Повторно використовуючи один об'єкт
function withValue(value) {
const d =
withValue.d ||
(withValue.d = {
enumerable: false,
writable: false,
configurable: false,
value,
});
// уникання подвійної роботи під час присвоєння значення
if (d.value !== value) d.value = value;
return d;
}
// і далі
Object.defineProperty(obj, "key", withValue("static"));
// якщо доступна операція заморожування, перешкоджає
// додаванню чи видаленню властивостей прототипа об'єкта
// (value, get, set, enumerable, writable, configurable)
(Object.freeze || Object)(Object.prototype);
Якщо властивість уже існує, метод Object.defineProperty()
спробує модифікувати властивість відповідно до значень в переданому дескрипторі та поточної конфігурації цієї властивості.
Якщо старий дескриптор має свій атрибут configurable
, встановлений у значення false
, то властивість називається неналаштовною. Не допускається змінювати будь-який з атрибутів неналаштовної аксесорної властивості, і також неможливо змінити тип властивості між властивістю даних і аксесорною властивістю. Для властивостей даних з атрибутом writable: true
допускається змінювати значення, а також змінювати значення атрибута writable
зі true
на false
. Буде викинуто помилку TypeError
під час спроби змінити атрибути неналаштовної властивості (за винятком атрибутів value
та writable
, якщо це дозволено), окрім ситуації, коли відбувається означення такого ж значення, як і попереднє значення властивості даних.
Коли поточна властивість є налаштовною, означення якогось атрибута як undefined
фактично його видаляє. Наприклад, нехай o.k
— аксесорна властивість. Виклик Object.defineProperty(o, "k", { set: undefined })
видалить наявний сетер, залишивши властивості k
самий лише гетер, і зробивши її доступною лише для прочитання. Якщо в новому дескрипторі якийсь атрибут відсутній, значення атрибута старого дескриптора зберігається (його не буде неявно перевизначено як undefined
). Існує можливість перемикатися між властивістю даних і аксесорною властивістю шляхом передачі нового дескриптора іншого виду. Наприклад, якщо новий дескриптор є дескриптором даних (містить атрибут value
чи writable
), то обидва попередні атрибути get
та set
дескриптора будуть відкинуті.
Приклади
Створення властивості
Коли вказана властивість не існує на об'єкті, то Object.defineProperty()
створює нову властивість, так, як описано. Поля дескриптора можуть бути опущені, замість них буде введено їхні усталені значення.
const o = {}; // Створює новий об'єкт
// Приклад додавання властивості об'єкта за допомогою
// defineProperty з дескриптором властивості даних
Object.defineProperty(o, "a", {
value: 37,
writable: true,
enumerable: true,
configurable: true,
});
// властивість 'a' існує всередині об'єкта o, і її значення дорівнює 37
// Приклад додавання властивості об'єкта за допомогою
// defineProperty з дескриптором аксесорної властивості
let bValue = 38;
Object.defineProperty(o, "b", {
get() {
return bValue;
},
set(newValue) {
bValue = newValue;
},
enumerable: true,
configurable: true,
});
o.b; // 38
// властивість 'b' існує в об'єкті o, і її значення дорівнює 38
// Значення властивості o.b тепер завжди ідентичне змінній bValue,
// доки властивість o.b не буде перевизначено
// Неможливо змішати обидва типи:
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get() {
return 0xdeadbeef;
},
});
// викидає TypeError: атрибут value мусить бути
// присутнім лише в дескрипторах даних,
// get — лише в аксесорних дескрипторах
Модифікування властивості
Під час внесення змін до наявної властивості, її поточна конфігурація визначає те, чи операція: буде успішною, не зробить нічого, чи викине TypeError
.
Атрибут writable
Коли атрибут writable
властивості встановлено у значення false
, цю властивість називають "незаписуваною". Їй неможливо присвоїти інше значення.
const o = {}; // Створює новий об'єкт
Object.defineProperty(o, "a", {
value: 37,
writable: false,
});
console.log(o.a); // 37
o.a = 25; // Помилка не викидається
// (в суворому режимі помилку буде викинуто,
// навіть якщо значення буде тим самим)
console.log(o.a); // 37; Присвоєння не спрацювало
// суворий режим
(() => {
"use strict";
const o = {};
Object.defineProperty(o, "b", {
value: 2,
writable: false,
});
o.b = 3; // викидає TypeError: "b" is read-only
return o.b; // повертає 2, якщо без попереднього рядка
})();
Як видно на прикладі, спроба записати щось у незаписувану властивість не змінить її, проте і не викине помилки.
Атрибут enumerable
Атрибут enumerable
властивості визначає те, чи буде властивість взята методом Object.assign()
або оператором розгортання. Для несимвольних
властивостей він також визначає те, чи з'явиться ця властивість в циклі for...in
та Object.keys()
, чи ні.
const o = {};
Object.defineProperty(o, "a", {
value: 1,
enumerable: true,
});
Object.defineProperty(o, "b", {
value: 2,
enumerable: false,
});
Object.defineProperty(o, "c", {
value: 3,
}); // усталене значення enumerable — false
o.d = 4; // усталене значення enumerable дорівнює true,
// якщо властивість створюється шляхом її встановлення
Object.defineProperty(o, Symbol.for("e"), {
value: 5,
enumerable: true,
});
Object.defineProperty(o, Symbol.for("f"), {
value: 6,
enumerable: false,
});
for (const i in o) {
console.log(i);
}
// Друкує 'a' та 'd' (завжди в такій послідовності)
Object.keys(o); // ['a', 'd']
o.propertyIsEnumerable("a"); // true
o.propertyIsEnumerable("b"); // false
o.propertyIsEnumerable("c"); // false
o.propertyIsEnumerable("d"); // true
o.propertyIsEnumerable(Symbol.for("e")); // true
o.propertyIsEnumerable(Symbol.for("f")); // false
const p = { ...o };
p.a; // 1
p.b; // undefined
p.c; // undefined
p.d; // 4
p[Symbol.for("e")]; // 5
p[Symbol.for("f")]; // undefined
Атрибут configurable
Атрибут configurable
керує тим, чи може властивість бути видалена, і одночасно чи можна змінювати інші її атрибути (окрім value
та writable
).
Коли його значенням встановлено false
, проте атрибут writable
дорівнює true
— value
все ж можна змінювати, і writable
все ще можна перемкнути з true
на false
. Коли його значення дорівнює true
, а writable
— false
, — то value
все ж можна замінити за допомогою defineProperty
(проте не операторами присвоєння), і сам атрибут writable
можна перемкнути.
const o = {};
Object.defineProperty(o, "a", {
get() {
return 1;
},
configurable: false,
});
Object.defineProperty(o, "a", {
configurable: true,
}); // викидає TypeError
Object.defineProperty(o, "a", {
enumerable: true,
}); // викидає TypeError
Object.defineProperty(o, "a", {
set() {},
}); // викидає TypeError (set до цього дорівнював undefined)
Object.defineProperty(o, "a", {
get() {
return 1;
},
}); // викидає TypeError
// (навіть якщо новий гетер робить точнісінько те саме)
Object.defineProperty(o, "a", {
value: 12,
}); // викидає TypeError // ('value' можна змінювати із 'configurable', встановленим у false, проте не в цьому випадку, через наявність аксесора 'get')
console.log(o.a); // 1
delete o.a; // Нічого не відбувається
console.log(o.a); // 1
Object.defineProperty(o, "b", {
writable: true,
configurable: false,
});
console.log(o.b); // undefined
Object.defineProperty(o, "b", {
value: 1,
}); // Навіть якщо configurable дорівнює false, значення досі можна замінювати, оскільки об'єкт залишається записуваним
console.log(o.b); // 1
Object.defineProperty(o, "b", {
writable: false,
});
Object.defineProperty(o, "b", {
value: 1,
}); // TypeError: властивість не можна змінити, оскільки воне не є ні записуваною, ні налаштовною
Якби атрибут configurable
властивості o.a
дорівнював true
, не було б викинуто жодну з цих помилок, і в кінці властивість було б видалено.
Додавання атрибутів і усталених значень
Важливо розуміти, яким чином застосовуються усталені значення атрибутів. Нерідко існує певна відмінність між застосуванням для присвоєння значень звертань до властивостей і вживанням методу Object.defineProperty()
, як проілюстровано у прикладі нижче.
const o = {};
o.a = 1;
// є еквівалентом до:
Object.defineProperty(o, "a", {
value: 1,
writable: true,
configurable: true,
enumerable: true,
});
// З іншого боку,
Object.defineProperty(o, "a", { value: 1 });
// є еквівалентом до:
Object.defineProperty(o, "a", {
value: 1,
writable: false,
configurable: false,
enumerable: false,
});
Застосування власних гетерів і сетерів
Наведений далі приклад показує, як реалізувати самоархівований об'єкт. Кожного разу, коли встановлюється нове значення властивості temperature
, масив archive
отримує новий запис.
function Archiver() {
let temperature = null;
const archive = [];
Object.defineProperty(this, "temperature", {
get() {
console.log("отримати!");
return temperature;
},
set(value) {
temperature = value;
archive.push({ val: temperature });
},
});
this.getArchive = () => archive;
}
const arc = new Archiver();
arc.temperature; // 'отримати!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
А в цьому прикладі гетер завжди повертає одне значення.
const pattern = {
get() {
return "Я завжди повертаю цей рядок, незалежно від того, що мені присвоєно";
},
set() {
this.myname = "це рядок із моїм іменем";
},
};
function TestDefineSetAndGet() {
Object.defineProperty(this, "myproperty", pattern);
}
const instance = new TestDefineSetAndGet();
instance.myproperty = "test";
console.log(instance.myproperty);
// Я завжди повертаю цей рядок, незалежно від того, що мені присвоєно
console.log(instance.myname); // це рядок із моїм іменем
Успадкування властивостей
Коли успадковується аксесорна властивість, її методи get
та set
викликаються, коли до цієї властивості звертаються чи модифікують її на дочірніх об'єктах. Якщо ці методи використовують змінну для зберігання значення, це значення поділятимуть всі об'єкти.
function MyClass() {}
let value;
Object.defineProperty(MyClass.prototype, "x", {
get() {
return value;
},
set(x) {
value = x;
},
});
const a = new MyClass();
const b = new MyClass();
a.x = 1;
console.log(b.x); // 1
Це можна виправити, якщо зберігати значення всередині іншої властивості. Всередині методів get
та set
значення this
вказує на об'єкт, який було використано для доступу чи зміни властивості.
function MyClass() {}
Object.defineProperty(MyClass.prototype, "x", {
get() {
return this.storedX;
},
set(x) {
this.storedX = x;
},
});
const a = new MyClass();
const b = new MyClass();
a.x = 1;
console.log(b.x); // undefined
На відміну від аксесорних властивостей, значення властивостей даних завжди встановлюються на самому об'єкті, а не на його прототипі. Проте, якщо успадковується незаписувана властивість даних, то це перешкодить внесенням змін до неї на дочірньому об'єкті.
function MyClass() {}
MyClass.prototype.x = 1;
Object.defineProperty(MyClass.prototype, "y", {
writable: false,
value: 1,
});
const a = new MyClass();
a.x = 2;
console.log(a.x); // 2
console.log(MyClass.prototype.x); // 1
a.y = 2; // Ігнорується, в суворому режимі викидає помилку
console.log(a.y); // 1
console.log(MyClass.prototype.y); // 1
Специфікації
Сумісність із браузерами
desktop | mobile | server | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
defineProperty
|
Chrome Full support 5 | Edge Full support 12 | Firefox Full support 4 | Internet Explorer Full support 9 | Opera Full support 11.6 | Safari Full support 5.1 | WebView Android Full support 1 | Chrome Android Full support 18 | Firefox for Android Full support 4 | Opera Android Full support 12 | Safari on iOS Full support 6 | Samsung Internet Full support 1.0 | Deno Full support 1.0 | Node.js Full support 0.10.0 |