Skip to main content

Прототипи та прототипне наслідування в JavaScript

Прототипне наслідування (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 могло повертати несподівані значення.

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

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

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

Дочитали статтю?