Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «OOP в JavaScript (об'єктно-орієнтоване програмування)». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**OOP в JavaScript** групує код в об'єкти, які ділять поведінку через ланцюжок прототипів (prototype chain), а не копіюють стан у кожен екземпляр. ```javascript class Animal { speak() { return 'звук'; } } class Dog extends Animal { speak() { return 'гавкіт'; } } console.log(new Dog().speak()); // "гавкіт" // dog -> Dog.prototype -> Animal.prototype ``` **Ключове:** `class` в ES6 - це синтаксичний цукор над прототипами. Чотири принципи: інкапсуляція, наслідування, поліморфізм, абстракція.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**OOP в JavaScript** групує дані та поведінку в об'єкти і використовує ланцюжок прототипів (prototype chain) для спільних методів, а не копіює стан у кожен екземпляр. ## Теорія ### TL;DR - Ланцюжок прототипів: `dog -> Dog.prototype -> Animal.prototype -> Object.prototype`. Перший збіг виграє. - Синтаксис `class` в ES6 - це синтаксичний цукор над прототипами. Під капотом нічого не змінилось. - Чотири принципи: інкапсуляція (ховаємо внутрішнє), наслідування (ділимося поведінкою), поліморфізм (один інтерфейс, різні реалізації), абстракція (ховаємо складність) - Класи - для ієрархій; `Object.create()` - для динамічного розширення; фабричні функції - коли наслідування не потрібне ### Швидкий приклад ```javascript class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} видає звук`; } } class Dog extends Animal { speak() { return `${this.name} гавкає`; } // перевизначає метод батьківського класу } const dog = new Dog('Рекс'); console.log(dog.speak()); // "Рекс гавкає" // Під капотом: // dog.__proto__ === Dog.prototype // Dog.prototype.__proto__ === Animal.prototype ``` `Dog` перевизначає `speak()` з `Animal`. Коли викликаєш `dog.speak()`, рушій знаходить метод на `Dog.prototype` і зупиняється. Це і є ланцюжок прототипів. ### Як працює ланцюжок прототипів Коли ти звертаєшся до `dog.speak()`, V8 проходить: сам об'єкт `dog`, потім `Dog.prototype`, потім `Animal.prototype`, потім `Object.prototype`, потім null. Перший збіг виграє. Це делегування, а не копіювання. Всі екземпляри `Dog` використовують один метод `speak` з `Dog.prototype`. Java копіює стан у кожен екземпляр. JavaScript - ні. `class` просто дає зручніший спосіб налаштувати цей ланцюжок, але сам ланцюжок існував завжди. ### Чотири принципи OOP **Інкапсуляція** ховає внутрішнє і надає контрольований інтерфейс. Приватні поля з `#` роблять це на рівні мови починаючи з ES2022: ```javascript class BankAccount { #balance = 0; constructor(initial) { this.#balance = initial; } deposit(amount) { if (amount > 0) this.#balance += amount; } getBalance() { return this.#balance; } } const account = new BankAccount(1000); account.deposit(500); console.log(account.getBalance()); // 1500 // account.#balance -> SyntaxError: private field outside class ``` **Наслідування** дозволяє дочірньому класу перевикористати і розширити батьківський. Перед зверненням до `this` треба викликати `super()`: ```javascript class Animal { constructor(name) { this.name = name; } move() { return `${this.name} рухається`; } } class Dog extends Animal { constructor(name, breed) { super(name); // обов'язково перед this.breed this.breed = breed; } fetch() { return `${this.name} приносить м'яч`; } } ``` **Поліморфізм** означає, що один і той самий метод поводиться по-різному залежно від того, який об'єкт його викликає: ```javascript class Shape { area() { throw new Error('area() не реалізовано'); } } class Circle extends Shape { constructor(r) { super(); this.r = r; } area() { return Math.PI * this.r ** 2; } } class Square extends Shape { constructor(s) { super(); this.s = s; } area() { return this.s ** 2; } } [new Circle(5), new Square(4)].forEach(s => console.log(s.area().toFixed(2))); // 78.54 // 16.00 ``` **Абстракція** ховає складність за простим інтерфейсом. Код, що викликає `query()`, не знає і не має знати, чи це MySQL чи PostgreSQL. ### Композиція проти наслідування Наслідування підходить для справжніх відносин "є" (is-a). `Dog` є `Animal`. Але коли потрібно поєднати поведінку з кількох різних джерел, наслідування стає незручним. Композиція будує об'єкти з маленьких шматків: ```javascript const canSwim = (state) => ({ swim: () => `${state.name} плаває` }); const canFly = (state) => ({ fly: () => `${state.name} літає` }); const Duck = (name) => { const state = { name }; return Object.assign({}, canSwim(state), canFly(state)); }; const duck = Duck('Дональд'); console.log(duck.swim()); // "Дональд плаває" console.log(duck.fly()); // "Дональд літає" ``` Ієрархії глибше 3-4 рівнів зазвичай стають крихкими. Якщо виникає бажання написати `super.super`, це сигнал перейти на композицію. ### Прототипи до ES6 ES6 нічого нового не вигадав. Ось той самий патерн на ES5: ```javascript function Person(name) { this.name = name; } Person.prototype.greet = function() { return `Привіт, я ${this.name}`; }; function Developer(name, lang) { Person.call(this, name); this.lang = lang; } Developer.prototype = Object.create(Person.prototype); Developer.prototype.constructor = Developer; Developer.prototype.code = function() { return `${this.name} пише ${this.lang}`; }; const dev = new Developer('Анна', 'JavaScript'); console.log(dev.greet()); // "Привіт, я Анна" console.log(dev.code()); // "Анна пише JavaScript" ``` Цей код ще зустрічається в старих проектах. `class`-версія компілюється приблизно в це. ### Коли використовувати OOP - Спільний стан між методами і чіткий зв'язок "є": класи з наслідуванням - Поведінка з кількох незв'язаних джерел: композиція - Нема спільного стану, просто згрупована логіка: фабричні функції або прості об'єкти - Більше 4-5 рівнів наслідування: переробити на композицію ### Типові помилки **Забути `super()` перед `this` в дочірньому конструкторі:** ```javascript // Неправильно - кине ReferenceError class Child extends Parent { constructor() { this.prop = 'child'; } } // Правильно class Child extends Parent { constructor() { super(); this.prop = 'child'; } } ``` Коли використовуєш `extends`, батьківський конструктор відповідає за створення об'єкта `this`. До виклику `super()` `this` ще не існує. **Мутація спільного прототипу:** ```javascript const proto = { greet() { return `Привіт, ${this.name}`; } }; const a = Object.create(proto); const b = Object.create(proto); proto.sayBye = () => 'Бувай'; // тепер і a, і b мають sayBye console.log(a.sayBye()); // "Бувай" console.log(b.sayBye()); // "Бувай" ``` Це prototype pollution. Саме через такий механізм спрацював lodash CVE-2018-3721. Заморожуй спільні об'єкти через `Object.freeze(proto)` або уникай мутабельних спільних прототипів. **Приватні поля і старі збирачі:** ```javascript class Secure { #secret = 42; } // Webpack нижче v5 не може розібрати цей синтаксис ``` Якщо не можеш оновити інструменти, використовуй `WeakMap`: ```javascript const _secrets = new WeakMap(); class Secure { constructor() { _secrets.set(this, 42); } getSecret() { return _secrets.get(this); } } ``` **Стрілкові функції як поля класу проти методів прототипу:** ```javascript class Counter { count = 0; increment = () => { this.count++; }; // окрема функція для кожного екземпляра decrement() { this.count--; } // спільна на прототипі, дешевша } ``` Обидва варіанти коректні. Стрілки вирішують проблему прив'язки `this` при передачі як колбек, але коштують більше пам'яті. Звичайні методи дешевші, але потребують `.bind()` на місці виклику. **Глибокі ланцюжки прототипів і продуктивність:** V8 добре кешує пошук у плоских структурах. Ланцюжки довші 5 рівнів призводять до промахів кешу. Це помітно в гарячих циклах; у звичайному коді програми різниця мінімальна. ### Як V8 обробляє це всередині V8 зберігає `[[Prototype]]` кожного об'єкта як прихований вказівник. Звернення до властивості запускає обхід `OrdinaryGetPrototypeOf` по ланцюжку. V8 кешує ці пошуки в прихованих класах (shapes) для повторного доступу. `new Dog()` встановлює `[[Prototype]]` на `Dog.prototype`. Ось і все, що робить `class`. ### Де зустрічається в реальних проектах - React: `class Component extends React.Component` для методів lifecycle. `PureComponent` - це просто `Component` з іншим `shouldComponentUpdate` на прототипі. - Node.js streams: `Readable`, `Transform`, `Duplex` ланцюжком через прототипи. `Transform extends Readable`. - Express: middleware як ланцюжки прототипів; `app.use()` делегує до прототипу роутера. - Lodash: утилітарні міксини через `Object.assign(proto, mixins)` для спільних методів. ### Питання на співбесіді **Q:** Яка різниця між полями класу і методами прототипу? **A:** Поля класу створюють власні властивості для кожного екземпляра: кожен об'єкт отримує свою копію в пам'яті. Методи прототипу спільні: одна функція на прототипі для всіх екземплярів. **Q:** Чому `super()` треба викликати перед `this` в дочірньому конструкторі? **A:** Коли використовуєш `extends`, батьківський конструктор відповідає за створення об'єкта `this`. До виклику `super()` `this` ще не існує, звернення до нього кидає `ReferenceError`. **Q:** Чим `Object.create(null)` відрізняється від `{}`? **A:** `{}` має `Object.prototype` в ланцюжку, тому успадковує `toString`, `hasOwnProperty` тощо. `Object.create(null)` не має прототипу взагалі. Корисно для чистих словників, де успадковані ключі можуть спричинити баги. **Q:** Чому `instanceof` іноді дає неправильні результати з `Object.create()`? **A:** `instanceof` перевіряє чи є `prototype` конструктора десь у ланцюжку прототипів об'єкта. Якщо ланцюжок зібраний вручну без прив'язки до конструктора, `instanceof` його не знайде. **Q:** (Senior) Як реалізувати приватний стан без полів `#`, і який компроміс? **A:** Використовуй `WeakMap` з екземпляром як ключем. Запис автоматично видаляється збирачем сміття, коли екземпляр знищується - витоку пам'яті немає. Symbol як ключ теж працює, але видимий через `Object.getOwnPropertySymbols()`. Поля `#` справді приватні: V8 хешує імена, щоб уникнути конфліктів між бандлами. ## Приклади ### Базовий: ланцюжок прототипів у дії ```javascript class Vehicle { constructor(make) { this.make = make; } describe() { return `Це ${this.make}`; } } class Car extends Vehicle { constructor(make, model) { super(make); this.model = model; } describe() { return `${super.describe()} ${this.model}`; // викликає Vehicle.describe() } } const car = new Car('Toyota', 'Camry'); console.log(car.describe()); // "Це Toyota Camry" // Перевірка ланцюжка вручну console.log(Object.getPrototypeOf(car) === Car.prototype); // true console.log(Object.getPrototypeOf(Car.prototype) === Vehicle.prototype); // true ``` `super.describe()` піднімається до `Vehicle.prototype` і викликає батьківський метод. Перевірки ланцюжка підтверджують те, що ES6 `class` налаштовує автоматично. ### Середній рівень: логер через композицію ```javascript class Logger { constructor(prefix) { this.prefix = prefix; this.logs = []; } log(message) { const entry = `[${this.prefix}] ${message}`; this.logs.push(entry); console.log(entry); return entry; } getLogs() { return [...this.logs]; } // повертаємо копію, а не сам масив } class UserService { constructor() { this.logger = new Logger('UserService'); // композиція: has-a Logger } login(user) { return this.logger.log(`Login: ${user.name}`); } logout(user) { return this.logger.log(`Logout: ${user.name}`); } } const service = new UserService(); service.login({ name: 'Аліса' }); service.logout({ name: 'Аліса' }); console.log(service.logger.getLogs()); // ["[UserService] Login: Аліса", "[UserService] Logout: Аліса"] ``` `UserService` використовує `Logger` через композицію, не наслідування. Якщо вимоги до логування зміняться, міняємо або розширюємо `Logger`, не чіпаючи `UserService`. Цей патерн я бачив як заміну цілих дерев наслідування в продакшені - там, де хтось спочатку запхав логер у базовий клас через `extends`. ### Просунутий рівень: prototype pollution ```javascript const sharedProto = { greet() { return `Привіт, ${this.name}`; } }; const user1 = Object.create(sharedProto); user1.name = 'Боб'; const user2 = Object.create(sharedProto); user2.name = 'Кароль'; // Мутація спільного прототипу впливає на всі об'єкти в ланцюжку sharedProto.isAdmin = () => true; console.log(user1.isAdmin()); // true - очікувано console.log(user2.isAdmin()); // true - user2 цього не просив ``` Саме так спрацював lodash CVE-2018-3721. JSON-пейлоад від атакувальника дістався до `__proto__` і ввів властивості в `Object.prototype`, вплинувши на всі об'єкти процесу Node.js. Заморожуй спільні прототипи через `Object.freeze(sharedProto)`, або використовуй `Object.create(null)` для контейнерів даних, які не мають нічого успадковувати.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.