Skip to main content

Пошук властивостей у ланцюжку прототипів

Ланцюжок прототипів (prototype chain) - це послідовність об'єктів, якою рушій JavaScript іде вгору, шукаючи властивість, якої немає в самому об'єкті. Пошук починається з власних властивостей, продовжується прототипом, прототипом прототипа і завершується на null.

Теорія

TL;DR

  • Читання obj.prop: спочатку власні властивості obj, потім obj.__proto__, потім вище, доки не null.
  • Не знайшли ніде - результат undefined, помилки немає.
  • Пошук динамічний: властивості не копіюються, рушій щоразу проходить ланцюжок (з кешуванням).
  • Запис obj.prop = value завжди створює власну властивість у obj, прототип лишається незмінним.
  • Геттери і сетери, знайдені в ланцюжку, виконуються, а не повертаються як значення.
  • Object.create(null) дає об'єкт із порожнім ланцюжком: пошук обривається одразу.

Швидкий приклад

javascript
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

Ланцюжок тут такий:

javascript
dog -> animal -> Object.prototype -> null

Візуально це виглядає як стос об'єктів, де кожен наступний є «запасним планом» для попереднього:

javascript
dog { barks: true } ^ | animal { eats: true } ^ | Object.prototype { toString(), hasOwnProperty(), ... } ^ | null

Алгоритм під капотом, покроково

Коли виконується obj.prop, рушій робить таке:

  1. Перевіряє, чи є prop власною властивістю obj. Якщо так, повертає її значення і зупиняється.

  2. Якщо ні, бере прототип об'єкта:

    javascript
    const parent = Object.getPrototypeOf(obj);
  3. Перевіряє, чи є prop у parent.

  4. Повторює кроки 2 і 3, доки:

    • властивість знайдено, або
    • прототип дорівнює null.

Якщо цикл дійшов до null, повертається undefined. Важливо, що все це відбувається динамічно, без копіювання властивостей: якщо ви додасте властивість у прототип пізніше, усі нащадки одразу її «побачать».

Запис властивості працює інакше

Під час obj.prop = value механізм інший:

  • Якщо властивість вже існує в самому об'єкті, вона просто перезаписується.
  • Якщо ні, у obj створюється нова власна властивість, а не в прототипі.
  • Прототип при цьому взагалі не змінюється.
javascript
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()), він виконується, а не повертається як функція. Аналогічно, знайдений сетер виконується під час запису, і тоді нова власна властивість в об'єкті не створюється.

javascript
const animal = { get info() { return 'Animal info'; } }; const dog = Object.create(animal); console.log(dog.info); // "Animal info", викликано геттер прототипа

Отже, прототипи можуть містити не лише дані, а й обчислювані, «реактивні» властивості.

Глибокі ланцюжки і оптимізація рушієм

Довжина ланцюжка не обмежена, пошук просто проходить більше рівнів:

javascript
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.

Об'єкт без прототипа

javascript
const pure = Object.create(null); pure.key = 'value'; console.log(pure.toString); // undefined, немає Object.prototype

Ланцюжок обривається одразу:

javascript
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') кине помилку.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.