Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Прототипи та прототипне наслідування в JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Прототипне наслідування** (prototypal inheritance) дозволяє об'єктам делегувати пошук властивостей іншим об'єктам через ланцюг `[[Prototype]]`-посилань. ```javascript const animal = { eat() { console.log('eating'); } }; const dog = Object.create(animal); // [[Prototype]] dog = animal dog.bark = () => console.log('woof'); dog.eat(); // делегує до animal dog.bark(); // власний метод ``` **Головне:** нічого не копіюється. JS проходить ланцюг у момент виконання, поки не знайде властивість або не дійде до `null`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Прототипне наслідування** (prototypal inheritance) у JavaScript означає, що об'єкти делегують пошук властивостей іншим об'єктам через ланцюг живих посилань, а не копіюють поведінку під час створення. ## Теорія ### TL;DR - Аналогія: просиш батька рецепт пасти. Не пам'ятає - питає у свого батька. Ланцюг закінчується на `null`. - Головна відмінність від класичного ООП: делегування під час виконання, а не копіювання під час створення. - V8 зберігає `[[Prototype]]` як прихований вказівник і проходить ланцюг при кожному зверненні. - `Object.create()` для явного делегування, класи для читабельних ієрархій (під капотом той самий механізм). - Ланцюги глибше 10 рівнів помітно уповільнюють пошук у V8. Тримай мілко. ### Швидкий приклад ```javascript const parent = { greeting: 'Hi' }; const child = Object.create(parent); // [[Prototype]] child = parent console.log(child.greeting); // 'Hi' — делегує вгору по ланцюгу child.surname = 'Smith'; console.log(child.surname); // 'Smith' — власна властивість console.log(parent.surname); // undefined — parent не змінився ``` У `child` немає власного `greeting`, тому JS піднімається до `parent` і знаходить його там. Запис `child.surname` створює власну властивість на `child`, а не на `parent`. Батьківський об'єкт залишається незайманим. ### Головна відмінність від класичного наслідування У Java або C++ батьківська поведінка копіюється в кожного нащадка під час створення. Нащадок з того моменту самодостатній. JavaScript робить навпаки: нічого не копіюється. Нащадок тримає живе посилання на батьківський об'єкт, і рушій проходить це посилання в момент виконання при кожному зверненні. Зміни в батькові одразу видно в нащадках - якщо ті не перекрили властивість своєю. ### Коли що використовувати - Спільні методи для багатьох екземплярів (як `Array.prototype.map`) - прототипи. - Фіксовані ієрархії з чіткою структурою і приватним станом - класи (вони все одно використовують прототипи всередині). - Динамічне розширення під час виконання для плагінів або mixins - `Object.create()`. - Критичні для продуктивності ділянки коду - уникай ланцюгів глибше 5 рівнів як безпечна межа. V8 документує деградацію після 10+ рівнів. ### Як це працює у V8 V8 зберігає `[[Prototype]]` як прихований вказівник на кожному об'єкті. При зверненні до властивості V8 запускає `GetProperty`, спочатку перевіряє власні ключі через inline-кеш (`LoadIC`). При промаху переходить по вказівнику до наступного об'єкта. Так до `null`, який повертає `undefined`. Є дві речі, що вбивають продуктивність. Виклик `Object.setPrototypeOf()` на існуючому об'єкті скасовує inline-кеші V8 і змушує рушій переоптимізовувати. Роби це один раз при створенні, не в циклах. Ланцюги глибше 10 об'єктів переводять inline-кеш у мегаморфний стан і уповільнюють пошук у 5-10 разів. ### Типові помилки **Мутація спільного прототипу в розрахунку на ізоляцію екземплярів.** ```javascript const proto = { count: 0 }; const a = Object.create(proto); const b = Object.create(proto); proto.count++; // і a, і b бачать цю зміну console.log(a.count, b.count); // 1 1 ``` Прототип спільний за посиланням, не за копією. Якщо кожен екземпляр повинен мати своє значення, встанови його безпосередньо на екземплярі: `a.count = 0`. **`for...in` проходить весь ланцюг.** ```javascript const obj = Object.create({ inherited: 'сюрприз' }); obj.own = 'моє'; for (let key in obj) console.log(key); // 'own', потім 'inherited' ``` Для власних властивостей використовуй `Object.keys()`, або додай `Object.prototype.hasOwnProperty.call(obj, key)` всередині циклу. **`Object.setPrototypeOf()` у циклі.** ```javascript let obj = {}; for (let i = 0; i < 100; i++) { Object.setPrototypeOf(obj, {}); // скасовує кеші V8 на кожній ітерації } ``` Створюй ланцюг прототипів один раз через `Object.create()`. Повторна мутація - це V8-пастка для продуктивності. **Припущення, що `instanceof` перевіряє лише один рівень.** ```javascript const plain = Object.create(null); // немає Object.prototype взагалі console.log(plain instanceof Object); // false ``` `Object.create(null)` створює чистий словник без жодного ланцюга. Корисно для hashmaps без ризику prototype pollution, але `instanceof`, `toString()` та подібні методи на ньому не спрацюють. ### Де зустрічається - React: синтетичні події (synthetic events) делегують до спільного прототипу для повторного використання пулу (ReactDOMSyntheticEvent). - Node.js: `http.Server` успадковує від `net.Server` через ланцюг прототипів - саме так методи EventEmitter доходять до HTTP-обробників. - Express: делегування через прототипи для спільних валідаторів і перевірки авторизації. - Vue.js: екземпляри компонентів розділяють спільний прототип опцій. - Кожен масив делегує `.map()`, `.filter()`, `.reduce()` з `Array.prototype`. ### Питання на співбесіді **Q:** Що виведе цей код? ```javascript const a = {}; const b = Object.create(a); a.x = 1; delete a.x; console.log(b.x); ``` **A:** `undefined`. Після `delete a.x` властивість зникла з `a`. `b` делегує до `a`, нічого не знаходить, повертає `undefined`. **Q:** Яка різниця між `__proto__` і `[[Prototype]]`? **A:** `[[Prototype]]` - внутрішній слот зі специфікації. `__proto__` - геттер/сеттер на `Object.prototype` для доступу до нього. В продакшені вважається застарілим. Використовуй `Object.getPrototypeOf()`. **Q:** Як ES6-класи пов'язані з прототипами? **A:** Класи - це синтаксичний цукор над тим самим механізмом. `class Person { greet() {} }` створює конструкторську функцію і записує `greet` в `Person.prototype`. `Object.getPrototypeOf(new Person()) === Person.prototype` завжди true. **Q:** Для чого корисний `Object.create(null)`? **A:** Створює об'єкт без жодного ланцюга прототипів. Немає успадкованих `toString`, `hasOwnProperty`, жодних прихованих ключів. Чистий словник, безпечний для довільних ключів без ризику prototype pollution. **Q:** (Senior) V8 оптимізує мілкі ланцюги. На якій глибині відбувається деоптимізація і яка реальна вартість? **A:** Приблизно 10-15 рівнів переводять inline-кеш у мегаморфний стан. Блог V8 документує уповільнення в ~5 разів на ланцюгу з 20 рівнів порівняно з 2-рівневим. Якщо будуєш систему плагінів з динамічними `Object.create()`-ланцюгами, виміряй це на реальних даних. ## Приклади ### Базове делегування через ланцюг прототипів ```javascript const vehicle = { type: 'vehicle', describe() { console.log(`Це ${this.type}`); } }; const car = Object.create(vehicle); car.type = 'car'; // перекриває vehicle.type const sportsCar = Object.create(car); // sportsCar не має власного type, делегує до car sportsCar.describe(); // 'Це car' console.log(Object.getPrototypeOf(sportsCar) === car); // true console.log(Object.getPrototypeOf(car) === vehicle); // true ``` `sportsCar` не має `describe`, тому рушій піднімається до `car`. У `car` теж немає, піднімається до `vehicle`, знаходить. Всередині виклику `this.type` вирішується як `car.type`, бо `sportsCar` делегує до `car`, де є ця власна властивість. ### Конструкторські функції і спільні методи Так спільні методи працювали до класів ES6. І це точний механізм, який класи використовують всередині. ```javascript function User(name, role) { this.name = name; this.role = role; } User.prototype.canEdit = function() { return this.role === 'admin'; }; User.prototype.greet = function() { console.log(`Привіт, я ${this.name}`); }; const alice = new User('Alice', 'admin'); const bob = new User('Bob', 'viewer'); alice.greet(); // 'Привіт, я Alice' console.log(alice.canEdit()); // true console.log(bob.canEdit()); // false // Обидва використовують одні й ті самі функції console.log(alice.canEdit === bob.canEdit); // true - одне посилання ``` `new User(...)` створює простий об'єкт і встановлює його `[[Prototype]]` на `User.prototype`. Функції живуть один раз на цьому об'єкті, не дублюються для кожного екземпляра. Саме в цьому аргумент щодо пам'яті: 1000 екземплярів розділяють один примірник кожного методу. ### Сюрприз від мутації прототипу в продакшені Цей баг зустрічається в реальних кодових базах, коли розробники змішують спільний стан прототипу з логікою конкретного екземпляра. ```javascript const SyntheticEventProto = { bubbles: false, preventDefault() { this.defaultPrevented = true; } }; const clickEvent = Object.create(SyntheticEventProto); clickEvent.type = 'click'; clickEvent.preventDefault(); console.log(clickEvent.defaultPrevented); // true const mouseEvent = Object.create(SyntheticEventProto); // Хтось мутує спільний прототип напряму SyntheticEventProto.bubbles = true; console.log(clickEvent.bubbles); // true - несподіваний спільний стан console.log(mouseEvent.bubbles); // true - той самий прототип, та сама зміна ``` `clickEvent.defaultPrevented` безпечний: `preventDefault()` встановлює `this.defaultPrevented`, що створює власну властивість на екземплярі. Але пряма мутація `SyntheticEventProto.bubbles` змінює спільний об'єкт. Кожен екземпляр без власного `bubbles` тепер бачить `true`. Реалізація React (ReactDOMSyntheticEvent) використовує схоже пулювання через прототипи - саме тому читання властивостей події асинхронно після React 16 могло повертати несподівані значення.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.