Пошук властивостей у ланцюжку прототипів
Ланцюжок прототипів (prototype chain) - це послідовність об'єктів, якою рушій JavaScript іде вгору, шукаючи властивість, якої немає в самому об'єкті. Пошук починається з власних властивостей, продовжується прототипом, прототипом прототипа і завершується на null.
Теорія
TL;DR
- Читання
obj.prop: спочатку власні властивостіobj, потімobj.__proto__, потім вище, доки неnull. - Не знайшли ніде - результат
undefined, помилки немає. - Пошук динамічний: властивості не копіюються, рушій щоразу проходить ланцюжок (з кешуванням).
- Запис
obj.prop = valueзавжди створює власну властивість уobj, прототип лишається незмінним. - Геттери і сетери, знайдені в ланцюжку, виконуються, а не повертаються як значення.
Object.create(null)дає об'єкт із порожнім ланцюжком: пошук обривається одразу.
Швидкий приклад
const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;
console.log(dog.barks); // 1) знайдено в dog -> true
console.log(dog.eats); // 2) немає в dog -> шукаємо в animal -> true
console.log(dog.toString); // 3) немає в animal -> Object.prototype -> [Function]
console.log(dog.fly); // 4) немає ніде -> undefinedЛанцюжок тут такий:
dog -> animal -> Object.prototype -> nullВізуально це виглядає як стос об'єктів, де кожен наступний є «запасним планом» для попереднього:
dog {
barks: true
}
^
|
animal {
eats: true
}
^
|
Object.prototype {
toString(), hasOwnProperty(), ...
}
^
|
nullАлгоритм під капотом, покроково
Коли виконується obj.prop, рушій робить таке:
-
Перевіряє, чи є
propвласною властивістюobj. Якщо так, повертає її значення і зупиняється. -
Якщо ні, бере прототип об'єкта:
javascriptconst parent = Object.getPrototypeOf(obj); -
Перевіряє, чи є
propуparent. -
Повторює кроки 2 і 3, доки:
- властивість знайдено, або
- прототип дорівнює
null.
Якщо цикл дійшов до null, повертається undefined. Важливо, що все це відбувається динамічно, без копіювання властивостей: якщо ви додасте властивість у прототип пізніше, усі нащадки одразу її «побачать».
Запис властивості працює інакше
Під час obj.prop = value механізм інший:
- Якщо властивість вже існує в самому об'єкті, вона просто перезаписується.
- Якщо ні, у
objстворюється нова власна властивість, а не в прототипі. - Прототип при цьому взагалі не змінюється.
const animal = { eats: true };
const dog = Object.create(animal);
dog.eats = false; // створює нову власну властивість у dog
console.log(dog.eats); // false, власна властивість
console.log(animal.eats); // true, у прототипі, не зміниласяТаке «перекриття» називають shadowing: власна властивість затуляє однойменну властивість прототипа.
Читання і запис ходять різними шляхами
| Операція | Де шукає | Що робить |
|---|---|---|
obj.prop (читання) | у obj, потім по ланцюжку вгору | повертає перше знайдене значення |
obj.prop = value (запис) | пише в obj, якщо writable: true | не змінює прототип |
delete obj.prop | лише в obj | не впливає на прототип |
Саме тому delete dog.eats після затуляння поверне доступ до animal.eats: видаляється власна властивість, а прототипна лишається на місці.
Геттери і сетери в ланцюжку
Якщо на будь-якому рівні ланцюжка знайдено геттер (get prop()), він виконується, а не повертається як функція. Аналогічно, знайдений сетер виконується під час запису, і тоді нова власна властивість в об'єкті не створюється.
const animal = {
get info() {
return 'Animal info';
}
};
const dog = Object.create(animal);
console.log(dog.info); // "Animal info", викликано геттер прототипаОтже, прототипи можуть містити не лише дані, а й обчислювані, «реактивні» властивості.
Глибокі ланцюжки і оптимізація рушієм
Довжина ланцюжка не обмежена, пошук просто проходить більше рівнів:
const a = { level: 'A' };
const b = Object.create(a);
const c = Object.create(b);
const d = Object.create(c);
console.log(d.level); // "A"Тут рушій перегляне чотири об'єкти, перш ніж знайде level в a. На практиці це не так дорого, як здається, бо JS-рушії (V8, SpiderMonkey та інші) активно оптимізують прототипний пошук:
- будують внутрішні «shape»-карти, тобто описи структури об'єктів;
- кешують, де саме була знайдена властивість (inline caches);
- скидають кеш, щойно змінюється прототип або набір властивостей.
Тому зміна прототипа під час виконання через Object.setPrototypeOf є повільною операцією: вона знецінює кеші і змушує рушій деоптимізувати код. Задавайте прототип одразу при створенні об'єкта: Object.create(proto) або class ... extends.
Об'єкт без прототипа
const pure = Object.create(null);
pure.key = 'value';
console.log(pure.toString); // undefined, немає Object.prototypeЛанцюжок обривається одразу:
pure -> nullПошук припиняється миттєво. Саме тому Object.create(null) зручний для словників: жоден ключ на кшталт toString чи constructor не «прийде» з прототипа і не зіпсує перевірку if (dict[key]).
Підсумок
| Етап | Що відбувається |
|---|---|
| 1 | Рушій шукає властивість у самому об'єкті |
| 2 | Якщо не знайдено, іде в [[Prototype]] |
| 3 | Продовжує підніматися ланцюжком |
| 4 | Дійшов до null - повертає undefined |
| Запис | Створює власну властивість в об'єкті, а не в прототипі |
| Аксесори | Геттери і сетери в ланцюжку теж враховуються і виконуються |
Типові помилки
- Думати, що властивості копіюються в об'єкт. Нічого не копіюється:
dogне міститьeats, він лише знаходить її вanimalпід час кожного читання. - Очікувати, що
dog.eats = falseзмінить прототип. Запис завжди створює власну властивість,animal.eatsлишитьсяtrue. - Плутати
inіhasOwnProperty.'eats' in dogдаєtrue(шукає по всьому ланцюжку), аObject.hasOwn(dog, 'eats')даєfalse, бо перевіряє лише власні властивості. - Перебирати об'єкт через
for...inбез фільтра. Цей цикл обходить і успадковані перелічувані властивості; використовуйтеObject.keysабоObject.hasOwn. - Змінювати прототип на льоту через
Object.setPrototypeOf. Це коректно, але повільно: рушій скидає кеші. Задавайте прототип під час створення. - Розширювати вбудовані прототипи. Додавання методів до
Object.prototypeчиArray.prototypeпотрапляє в ланцюжок геть усіх об'єктів і ламає чужий код. - Забувати про
Object.create(null). Такий об'єкт не маєtoString,hasOwnPropertyі подібних методів, томуpure.hasOwnProperty('key')кине помилку.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.