В першій главі цього розділу, ми згадували сучасні методи роботи з прототипом.
Встановлення або читання прототипу за допомогою obj.__proto__
вважається застарілим і не рекомендуються для використання в майбутньому (перенесено до так званого “Annex B” стандарту JavaScript, призначеного лише для браузерів).
Сучасні методи отримання/встановлення прототипу:
- Object.getPrototypeOf(obj) – повертає значення
[[Prototype]]
об’єктуobj
. - Object.setPrototypeOf(obj, proto) – встановлює значення
[[Prototype]]
об’єктуobj
рівнеproto
.
Єдине використання __proto__
, яке не викликає негативного ставлення, це як властивість під час створення нового об’єкта: { __proto__: ... }
.
Хоча і для цього є спеціальний метод:
- Object.create(proto[, descriptors]) – створює порожній об’єкт із заданим
proto
як[[Prototype]]
і необов’язковими дескрипторами властивостей.
Наприклад:
let animal = {
eats: true
};
// створюється новий об’єкт з прототипом animal
let rabbit = Object.create(animal); // same as {__proto__: animal}
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // змінює прототип об’єкту rabbit на {}
Object.create
має необов’язковий другий аргумент: дескриптори властивостей.
Ми можемо надати додаткові властивості новому об’єкту, як тут:
let animal = {
eats: true
};
let rabbit = Object.create(animal, {
jumps: {
value: true
}
});
alert(rabbit.jumps); // true
Формат дескрипторів описаний в цій главі Прапори та дескриптори властивостей.
Ми можемо використати Object.create
, щоб клонувати об’єкт ефективніше ніж циклом for..in
:
let clone = Object.create(
Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)
);
Таким чином ми створюємо справжню копію об’єкта obj
, включаючи всі властивості: перелічувані та не перелічувані, сетери/гетери – з правильним значенням [[Prototype]]
.
Коротка історія
Якщо порахувати всі способи керування властивістю [[Prototype]]
, їх буде багато! Багато способів робити одне й те ж саме! Як так сталося? Чому?
Так склалося історично.
Прототипна спадковість була в мові з самого початку, але способи керування нею розвивалися з часом.
- Властивість
prototype
функції-конструктора працювала з дуже давніх часів. Це найстаріший спосіб створення об’єктів із заданим прототипом. - Пізніше, в 2012 році, метод
Object.create
став стандартом. Це дало можливість створювати об’єкти з певним прототипом, проте не дозволяло отримувати або встановлювати його. Тому браузери реалізували не стандартний аксесор__proto__
, що дозволяв користувачу отримувати та встановлювати прототип в будь-який час, щоб надати розробникам більше гнучкості. - Ще пізніше, в 2015 році, методи
Object.setPrototypeOf
таObject.getPrototypeOf
були додані до стандарту, для того, щоб виконувати аналогічну функціональність як і__proto__
. Оскільки__proto__
було широко реалізовано, воно згадується в Annex B стандарту як не обов’язкове для не-браузерних середовищ, але вважається свого роду застарілим. - Пізніше, у 2022 році, було офіційно дозволено використовувати
__proto__
в об’єктних літералах{...}
(винесено з Annex B), але не як геттер/сеттерobj.__proto__
(ця можливість все ще в Annex B).
Чому __proto__
було замінено методами getPrototypeOf/setPrototypeOf
?
Чому __proto__
було частково відновлено і його використання дозволено в {...}
, але не як геттер/сеттер?
Це цікаве запитання, яке вимагає від нас розуміння, чому __proto__
поганий.
І незабаром ми отримаємо відповідь.
[[Prototype]]
в існуючих об’єктів, якщо швидкість важливаТехнічно, ми можемо отримати/встановити [[Prototype]]
в будь-який момент. Але зазвичай ми тільки встановлюємо його один раз під час створення об’єкту та більше не змінюємо: rabbit
наслідується від animal
і це не зміниться.
JavaScript рушій є високо оптимізованим для цього. Зміна прототипу “на льоту” за допомогою Object.setPrototypeOf
або obj.__proto__=
дуже повільна операція, яка порушує внутрішню оптимізацію для операції з доступу до властивості об’єкта. Щоб уникнути цього, ви маєте розуміти що ви робите і для чого або швидкодія виконання JavaScript коду повністю не важлива для вас.
"Прості" об’єкти
Як ми знаємо, об’єкти можуть використовуватися як асоціативні масиви для зберігання пар ключ/значення.
…Проте якщо ми спробуємо зберегти створені користувачем ключі в ньому (наприклад, словник з користувацьким вводом), ми можемо спостерігати цікавий збій: усі ключі працюють правильно окрім "__proto__"
.
Розгляньте приклад:
let obj = {};
let key = prompt("Введіть ключ", "__proto__");
obj[key] = "певне значення";
alert(obj[key]); // [object Object], не "певне значення"!
Тут, якщо користувач вводить __proto__
, призначення в рядку 4 ігнорується!
Це, безумовно, може бути дивним для нерозробника, але досить зрозумілим для нас. Властивість __proto__
є особливою: вона має бути або об’єктом, або null
. Рядок не може стати прототипом. Ось чому призначення рядка __proto__
ігнорується.
Проте ми не намагалися реалізувати таку поведінку. Ми хотіли зберегти пари ключ/значення і при цьому ключ з назвою "__proto__"
не зберігся. Тому це помилка!
Тут наслідки не такі страшні. Але в інших випадках ми можемо зберігати об’єкти замість рядків у obj
, і тоді прототип справді буде змінено. У результаті код відпрацює зовсім несподіваним чином.
Найгірше в цьому – зазвичай розробники не задумаються над тим, що така ситуація взагалі можлива. Це робить подібні помилки важкими для виявлення і перетворюються їх у вразливості, особливо коли JavaScript використовується на стороні серверу.
Неочікувані речі також можуть статися під час призначення obj.toString
, оскільки це вбудований метод об’єкта.
Як ми можемо уникнути цієї проблеми?
В першу чергу, для зберігання ми можемо просто використовувати Map
замість простих об’єктів, тоді все працює правильно:
let map = new Map();
let key = prompt("What's the key?", "__proto__");
map.set(key, "some value");
alert(map.get(key)); // "some value" (як і передбачалося)
…Але синтаксис Object
часто більш привабливий, оскільки він більш стислий.
На щастя, ми можемо використовувати об’єкти, тому що творці мови давно подумали про цю проблему.
__proto__
не є властивістю об’єкту, але є аксесором Object.prototype
:
Таким чином, якщо obj.__proto__
зчитується або встановлюється, відповідний гетер/сетер викликається з прототипу та отримує/встановлює [[Prototype]]
.
Як було сказано на початку цього розділу: __proto__
це спосіб доступу до [[Prototype]]
, але не є самим [[Prototype]]
.
Тепер, коли ми хочемо використати об’єкт як асоціативний масив та уникнути таких проблем, ми можемо зробити це за допомогою невеликої хитрості:
let obj = Object.create(null);
// or: obj = { __proto__: null }
let key = prompt("Введіть ключ", "__proto__");
obj[key] = "певне значення";
alert(obj[key]); // "певне значення"
Object.create(null)
створює пустий об’єкт без прототипу ([[Prototype]]
дорівнює null
):
Таким чином, відсутні наслідувані гетер/сетер для __proto__
. Тепер ми працюємо зі звичайною властивістю, тому приклад вище працює правильно.
Ми можемо називати такі об’єкти “простими” або “чистим словниковим” об’єктом, тому що вони навіть простіше ніж звичайні об’єкти {...}
.
Недоліком є те, що такі об’єкти не мають вбудовані методи для роботи з ними, наприклад toString
:
let obj = Object.create(null);
alert(obj); // Помилка (відсутній метод toString)
…Але це зазвичай нормально для асоціативних масивів.
Зверніть увагу, що більшість методів пов’язаних з об’єктом Object.something(...)
, такі як Object.keys(obj)
– не розміщуються в прототипі, тому вони будуть працювати з такими об’єктами:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
alert(Object.keys(chineseDictionary)); // hello,bye
Підсумки
-
Щоб створити об’єкт із заданим прототипом, використовуйте:
- літеральний синтаксис:
{ __proto__: ... }
, дозволяє вказати кілька властивостей - або Object.create(proto[, descriptors]), дозволяє вказати дескриптори властивостей.
Object.create
забезпечує простий спосіб поверхневого копіювання об’єкта з усіма дескрипторами:let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
- літеральний синтаксис:
-
Сучасні методи отримання/встановлення прототипу:
- Object.getPrototypeOf(obj) – returns the
[[Prototype]]
ofobj
(same as__proto__
getter). - Object.setPrototypeOf(obj, proto) – sets the
[[Prototype]]
ofobj
toproto
(same as__proto__
setter).
- Object.getPrototypeOf(obj) – returns the
-
Отримання/встановлення прототипу за допомогою вбудованого
__proto__
геттера/сеттера не рекомендується, тепер це в Annex B специфікації. -
Ми також розглядали об’єкти без прототипів, створені за допомогою
Object.create(null)
або{__proto__: null}
.Ці об’єкти використовуються як сховища ключ-значення для зберігання будь-яких (можливо, створених користувачем) ключів.
Зазвичай об’єкти успадковують вбудовані методи та
__proto__
геттер/сеттер відObject.prototype
, роблячи відповідні ключі “зайнятими”, і це потенційно викликає побічні ефекти. З прототипомnull
об’єкти справді порожні.