Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Класи і прототипи». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Клас у JavaScript, це синтаксичний цукор над функцією-конструктором і прототипним успадкуванням: жодної окремої моделі об'єктів він не вводить.** Оголошення класу створює функцію (`typeof User === 'function'`), а всі методи кладе не в екземпляр, а в `User.prototype`; `new User()` створює порожній об'єкт, ставить йому `[[Prototype]] = User.prototype` і викликає конструктор. `extends` лише зв'язує два ланцюжки: `Child.prototype` вказує на `Parent.prototype`, а сам `Child` на `Parent` (для статичних членів). ```javascript class User { sayHi() {} } const u = new User(); console.log(typeof User); // 'function' console.log(Object.getPrototypeOf(u) === User.prototype); // true console.log(Object.getOwnPropertyNames(User.prototype)); // ['constructor', 'sayHi'] ``` **Ключове:** клас дає зручний синтаксис, але пошук методів усе одно йде вгору по ланцюжку прототипів, і саме цю перевірку виконує `instanceof`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Класи в JavaScript, це зручна обгортка над функціями-конструкторами та прототипами: мова під ними не змінилася, змінився лише синтаксис.** Клас створює функцію-конструктор, кладе методи в її `prototype` і, за наявності `extends`, будує ланцюжок прототипів. ## Теорія ### TL;DR - `class`, це функція: `typeof User === 'function'`. - Методи класу записуються в `User.prototype`, а не в кожен екземпляр. - `new User()` створює об'єкт, ставить йому прототипом `User.prototype` і викликає конструктор. - `extends` зв'язує `Child.prototype` з `Parent.prototype`, а `Child` з `Parent` (для статичних членів). - Виклик методу, це пошук угору по ланцюжку прототипів; `instanceof` перевіряє той самий ланцюжок. - Цукор не повністю прозорий: клас строгий, його методи неперелічувані, і без `new` він не викликається. ### Швидкий приклад ```javascript class User { constructor(name) { this.name = name; } sayHi() { console.log(`Привіт, ${this.name}!`); } } // Приблизний еквівалент до появи class: function UserOld(name) { this.name = name; } UserOld.prototype.sayHi = function () { console.log(`Привіт, ${this.name}!`); }; ``` Обидва варіанти дають однакову структуру: дані в екземплярі, методи в прототипі. ### Що відбувається при `new` Коли ви пишете `const user = new User('Maria')`, рушій виконує чотири кроки: 1. Створює новий порожній об'єкт. 2. Встановлює його внутрішній `[[Prototype]]` рівним `User.prototype`. 3. Викликає конструктор із `this`, що вказує на цей об'єкт. 4. Повертає об'єкт, якщо конструктор явно не повернув інший об'єкт. ```javascript const user = new User('Maria'); console.log(Object.getPrototypeOf(user) === User.prototype); // true console.log(user.constructor === User); // true console.log(Object.keys(user)); // ['name'], лише дані ``` Екземпляр зберігає тільки власні дані, а поведінку бере з прототипу. ### Методи, це властивості `prototype` Кожен метод, оголошений у тілі класу, автоматично потрапляє в `Class.prototype`, тому всі екземпляри поділяють одну функцію в пам'яті. ```javascript console.log(Object.getOwnPropertyNames(User.prototype)); // ['constructor', 'sayHi'] const u1 = new User('Maria'); const u2 = new User('Oleh'); console.log(u1.sayHi === u2.sayHi); // true, один метод на всіх ``` Це і є головна ідея прототипів: не дублювати функції в кожному об'єкті. Виняток, це поля класу (`name = 'default'`) та методи, оголошені як стрілкові властивості: вони створюються для кожного екземпляра окремо. ### `extends`, це прототипне успадкування `extends` не робить нічого нового, він лише зв'язує прототипи. ```javascript class Animal { speak() { console.log('The animal makes a sound'); } } class Dog extends Animal { bark() { console.log('Woof woof!'); } } const d = new Dog(); d.speak(); // 'The animal makes a sound' ``` Під капотом відбувається приблизно таке: ```javascript // зв'язок прототипів екземплярів: методи Object.setPrototypeOf(Dog.prototype, Animal.prototype); // зв'язок самих конструкторів: статичні члени Object.setPrototypeOf(Dog, Animal); ``` Звідси два паралельні ланцюжки. Для екземплярів: ``` d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null ``` Для конструкторів: ``` Dog -> Animal -> Function.prototype -> Object.prototype -> null ``` Перевірити це можна прямо в коді: ```javascript console.log(Object.getPrototypeOf(d) === Dog.prototype); // true console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true console.log(Object.getPrototypeOf(Dog) === Animal); // true ``` Другий ланцюжок пояснює, чому статичні методи батька доступні через дочірній клас: `Dog.someStaticMethod()` знаходиться на `Animal`. ### Як шукається метод і що перевіряє `instanceof` Виклик `d.speak()` виконується так: 1. Чи є власна властивість `speak` у самому `d`? Немає. 2. Рушій дивиться в `Dog.prototype`. Немає. 3. Далі в `Animal.prototype`. Знайшов, викликає з `this === d`. 4. Якби не знайшов, пішов би в `Object.prototype`, а потім отримав би `undefined`. Той самий ланцюжок обходить `instanceof`: він перевіряє, чи зустрічається `Constructor.prototype` серед прототипів об'єкта. ```javascript console.log(d instanceof Dog); // true console.log(d instanceof Animal); // true, бо Animal.prototype є в ланцюжку console.log(d instanceof Object); // true ``` Ключове слово `super` теж працює через прототипи: `super.speak()`, це виклик методу з `Object.getPrototypeOf(Dog.prototype)` із поточним `this`, а `super(...)` у конструкторі викликає батьківський конструктор, який і створює `this` для похідного класу. ### Що клас робить під капотом і чим усе ж відрізняється Еквівалент без `class` виглядає так: ```javascript function Animal() {} Animal.prototype.move = function () {}; function Dog() { Animal.call(this); } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function () {}; const dog = new Dog(); dog.bark(); ``` | Конструкція класу | Що відбувається під капотом | | --- | --- | | `constructor` | тіло функції-конструктора | | метод класу | властивість у `Class.prototype` | | статичний метод | властивість на самій функції `Class` | | поле класу | власна властивість екземпляра, створена в конструкторі | | `extends` | `Child.prototype` вказує на `Parent.prototype`, `Child` на `Parent` | | `super()` | виклик батьківського конструктора, який створює `this` | | `instanceof` | перевірка ланцюжка прототипів | Але цукор не зовсім прозорий, і про ці відмінності питають на співбесідах: - Код класу завжди виконується в strict mode. - Методи класу неперелічувані, тому не з'являються у `for...in`. - Клас не можна викликати без `new`: буде `TypeError`. - Оголошення класу не піднімається як `function`: до рядка оголошення воно в TDZ. - У похідному класі `this` недоступний до виклику `super()`. ### Типові помилки - **Вважати клас окремою моделлю ООП.** У JavaScript немає класів у розумінні Java чи C#: є лише об'єкти і посилання на прототипи. - **Плутати `User.prototype` і `Object.getPrototypeOf(User)`.** Перше, прототип майбутніх екземплярів, друге, прототип самої функції-класу, тобто звідки беруться статичні члени. - **Думати, що методи копіюються в екземпляр.** Вони лежать в одному прототипі, тому `u1.sayHi === u2.sayHi`. - **Викликати клас без `new`.** Функцію-конструктор старого стилю так викликати можна (і зіпсувати `this`), а клас кине `TypeError`. - **Присвоювати поля стрілковим функціям без потреби.** `handle = () => {}` створює нову функцію для кожного екземпляра і не потрапляє в прототип, тож пам'ять зростає. - **Забути `super()` у похідному конструкторі.** Без нього звернення до `this` дасть `ReferenceError`, бо об'єкт ще не створено.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.