Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Пошук властивостей у ланцюжку прототипів». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Коли ви читаєте `obj.prop`, рушій спочатку шукає власну властивість у самому об'єкті, потім у його прототипі (`Object.getPrototypeOf(obj)`), потім у прототипі прототипа і так далі вгору, доки не знайде властивість або не дійде до `null`.** Якщо властивості немає ніде, результат `undefined`. Ця послідовність і називається ланцюжок прототипів (prototype chain). Пошук динамічний: властивості нікуди не копіюються. Запис працює інакше: `obj.prop = value` завжди створює або перезаписує власну властивість у самому `obj` і прототип не чіпає, за винятком випадку, коли в ланцюжку знайдено сетер. ```javascript const animal = { eats: true }; const dog = Object.create(animal); console.log(dog.eats); // true, знайдено в animal console.log(dog.fly); // undefined, немає ніде ``` **Ключове:** читання йде вгору по ланцюжку, запис зупиняється на самому об'єкті.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Ланцюжок прототипів (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')` кине помилку.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.