В першій главі цього розділу, ми згадували сучасні методи роботи з прототипом.
Властивість __proto__ вважається застарілою (підтримується браузером відповідно до стандарту).
Сучасні методи:
- Object.create(proto, [descriptors]) – створює пустий об’єкт з властивістю
[[Prototype]], що посилається на переданий об’єктproto, та необов’язковими для передачі дескрипторами властивостейdescriptors. - Object.getPrototypeOf(obj) – повертає значення
[[Prototype]]об’єктуobj. - Object.setPrototypeOf(obj, proto) – встановлює значення
[[Prototype]]об’єктуobjрівнеproto.
Ці методи необхідно використовувати на відміну від __proto__.
Наприклад:
let animal = {
eats: true
};
// створюється новий об’єкт з прототипом animal
let rabbit = Object.create(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__було широко реалізовано, воно згадується в Додатку B стандарту як не обов’язкове для не-браузерних середовищ, але вважається свого роду застарілим.
Таким чином зараз ми маємо всі ці способи для роботи з прототипом.
Чому __proto__ було замінено методами getPrototypeOf/setPrototypeOf? Це цікаве питання, яке вимагає від нас розуміння чому __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__, присвоєння проігнорується!
Це не повинно дивувати нас. Властивість __proto__ є особливою: вона має бути або об’єктом або null. Рядок не може стати прототипом.
Проте ми не намагалися реалізувати таку поведінку. Ми хотіли зберегти пари ключ/значення і при цьому ключ з назвою "__proto__" не зберігся. Тому це помилка!
В цьому прикладі наслідки не такі жахливі. Однак в інших випадках ми можемо встановлювати об’єктні значення і тоді прототип може дійсно змінитись. В результаті, виконання коду піде неправильним та неочікуваним шляхом.
Найгірше в цьому – зазвичай розробники не задумаються над тим, що така ситуація взагалі можлива. Це робить подібні помилки важкими для виявлення і перетворюються їх у вразливості, особливо коли JavaScript використовується на стороні серверу.
Неочікувані речі можуть траплятися при встановлені значення для властивості toString, яка за замовчуванням є функцією, та для інших вбудованих методів.
Як ми можемо уникнути цієї проблеми?
В першу чергу, для зберігання ми можемо просто використовувати Map замість простих об’єктів, тоді все працює правильно.
Проте Object може також послужити нам, оскільки творці мови задумувались над цією проблемою вже дуже давно.
__proto__ не є властивістю об’єкту, але є аксесором Object.prototype:
Таким чином, якщо obj.__proto__ зчитується або встановлюється, відповідний гетер/сетер викликається з прототипу та отримує/встановлює [[Prototype]].
Як було сказано на початку цього розділу: __proto__ це спосіб доступу до [[Prototype]], але не є самим [[Prototype]].
Тепер, коли ми хочемо використати об’єкт як асоціативний масив та уникнути таких проблем, ми можемо зробити це за допомогою невеликої хитрості:
let obj = Object.create(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
Підсумки
Сучасними методами встановлення та прямого доступу до прототипу є:
- Object.create(proto, [descriptors]) – створює пустий об’єкт з переданим
protoяк[[Prototype]](може дорівнюватиnull) та необов’язковими дескрипторами властивостей. - Object.getPrototypeOf(obj) – повертає
[[Prototype]]об’єктуobj(так само як і гетер__proto__). - Object.setPrototypeOf(obj, proto) – встановлює
[[Prototype]]об’єктуobjзначенняproto(так само як і сетер__proto__).
Вбудований гетер/сетер __proto__ небезпечний, якби ми захотіли помістити згенеровані користувачем ключі в об’єкт. Тому що користувач може ввести "__proto__" як ключ і буде помилка, з неочікуваними наслідками.
Тому ми можемо використовувати як і Object.create(null), щоб створити “простий” об’єкт без __proto__, так і Map для цього.
Також, Object.create надає простий спосіб створити копію об’єкту з усіма дескрипторами:
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
Ми також з’ясували що __proto__ є гетер/сетер для [[Prototype]] та існує в Object.prototype, як і інші методи.
Ми можемо створити об’єкт без прототипу за допомогою Object.create(null). Такі об’єкти використовуються як “чисті словники”, у них відсутні проблеми з "__proto__" як ключем.
Інші методи:
- Object.keys(obj) / Object.values(obj) / Object.entries(obj) – повертає масив перелічуваних власних рядкових властивостей ключі/значення/пари ключ-значення.
- Object.getOwnPropertySymbols(obj) – повертає масив всіх власних символьних ключів.
- Object.getOwnPropertyNames(obj) – повертає масив всіх власних рядкових ключів.
- Reflect.ownKeys(obj) – повертає масив усіх власних ключів.
- obj.hasOwnProperty(key): повертає
trueякщоobjмає власний (не наслідуваний) ключ з назвоюkey.
Всі методи, що повертають властивості об’єкта (такі як Object.keys і інші) – повертають “власні” властивості. Якщо нам потрібні також наслідувані властивості, ми можемо використовувати цикл for..in.


Коментарі
<code>, для кількох рядків – обгорніть їх тегом<pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)