Спадкування класів у JavaScript

Тепер поговоримо про спадкування на рівні класів, тобто коли об'єкти, створювані, наприклад, через new Admin, повинні мати всі методи, які є у об'єктів, що створюються через new User, і ще якісь свої.

Спадкування Array від Object

Погляньмо на нього ще раз на прикладі Array, який успадковує від Object:

prototype

  • Методи масивів Array зберігаються в Array.prototype.
  • Array.prototype має прототипом Object.prototype.

Тому коли екземпляри класу Array хочуть отримати метод масиву – вони беруть його зі свого прототипу, наприклад Array.prototype.slice.

Якщо ж потрібний метод об'єкта, наприклад, маєOwnProperty , то його в Array.prototype немає, і він береться з Object.prototype .

Відмінний спосіб «помацати це руками» - запустити в консолі команду console.dir([1,2,3]).

Висновок у Chrome буде приблизно таким:

спадкування

Тут виразно видно, що самі дані та length знаходяться в масиві, далі в __proto__ йдуть методи для масивів concat, тобто Array.prototype, а далі – Object.prototype.

Зверніть увагу, я використовував саме console.dir, а не console.log, оскільки log часто виводить об'єкт у вигляді рядка, без доступу до властивостей.

Спадкування у наших класах

Застосуємо той самий підхід для наших класів: оголосимо клас Rabbit, який буде успадковувати від Animal.

Спочатку створимо два ці класи окремо, вони поки що будуть абсолютно незалежними.

Для того, щоб успадкування працювало, об'єкт rabbit = new Rabbit повинен використовувати властивості та методи зі свого прототипу Rabbit.prototype, а якщо їх там немає, то – властивості та методи батьків, які зберігаються в Animal.prototype.

Якщо ще коротше – порядок пошуку властивостей та методівмає бути таким: rabbit -> Rabbit.prototype -> Animal.prototype , за аналогією до того, як це зроблено для об'єктів і масивів.

Для цього можна поставити посилання __proto__ з Rabbit.prototype на Animal.prototype.

Можна зробити це так:

Однак прямий доступ до __proto__ не підтримується в IE10-, тому для підтримки цих браузерів ми використовуємо функцію Object.create . Вона або вбудована або легко емулюється у всіх браузерах.

Клас Animal залишається без змін, а Rabbit.prototype ми створюватимемо з потрібним прототипом, використовуючи Object.create :

Тепер виглядатиме ієрархія так:

prototype

У prototype за умовчанням завжди знаходиться властивість конструктора, що вказує на функцію-конструктор. Зокрема, Rabbit.prototype.constructor == Rabbit . Якщо ми розраховуємо використовувати цю властивість, то при заміні prototype через Object.create потрібно її явно зберегти:

Повний код успадкування

Для наочності - ось підсумковий код з двома класами Animal і Rabbit:

Як видно, успадкування задається лише одним рядком, поставленим у правильному місці.

У деяких застарілих посібниках пропонують замість Object.create(Animal.prototype) записувати в прототип new Animal, ось так:

Частково він робітник, оскільки ієрархія прототипів буде така ж, адже new Animal – це об'єкт з прототипом Animal.prototype, як і Object.create(Animal.prototype). Вони у цьому плані ідентичні.

Але цей підхід має важливий недолік. Як правило ми не хочемо створювати Animal, а хочемо лише успадкувати його методи!

Більше того, на практиці створення об'єкта може вимагати обов'язкових аргументів, впливати на сторінку в браузері, робити запити до сервера і ще щось, чого ми хотіли б уникнути.Тому рекомендується використовувати варіант з Object.create.

Виклик конструктора батька

Подивимося уважно на конструктори Animal та Rabbit із прикладів вище:

Як видно, об'єкт Rabbit не додає жодної особливої ​​логіки при створенні, якої не було в Animal.

Щоб спростити підтримку коду, є сенс не дублювати код конструктора Animal, а безпосередньо викликати його:

Такий виклик запустить функцію Animal в контексті поточного об'єкта, з усіма аргументами, вона виконається і запише в це все, що потрібно.

Тут можна було б використовувати і Animal.call(this, name), але apply надійніше, оскільки працює з будь-якою кількістю аргументів.

Перевизначення методу

Отже, Rabbit успадковує Animal. Тепер якщо якогось методу немає в Rabbit.prototype - він буде взятий з Animal.prototype.

У Rabbit може знадобитися задати якісь методи, які батьки вже мають. Наприклад, кролики бігають не так, як решта тварин, тому перевизначимо метод run() :

Виклик rabbit.run() тепер братиме run зі свого прототипу:

javascript

Виклик методу батька всередині свого

Частіша ситуація - коли ми хочемо не просто замінити метод на свій, а взяти метод батька і розширити його. Скажімо, кролик біжить так само, як інші звірі, але час від часу підстрибує.

Для виклику методу батька можна звернутися безпосередньо до нього, взявши з прототипу:

Зверніть увагу на виклик через apply та явну вказівку контексту.

Якщо викликати просто Animal.prototype.run() , то як ця функція run отримає Animal.prototype , а це невірно, потрібен поточний об'єкт.

Для успадкування потрібно, щоб "склад методів нащадка" (Child.prototype) успадковував від "складу методу батьків" (Parent.prototype).

Це можна зробити за допомогою Object.create:

Для того, щоб спадкоємець створювався так само, як і батько, він викликає конструктор батька у своєму контексті, використовуючи apply (this, arguments), ось так:

При перевизначенні методу батька нащадку, до вихідного методу можна звернутися, взявши його з прототипу:

Структура успадкування повністю:

Таке успадкування краще функціонального стилю, оскільки не дублює методи у кожному об'єкті.

Крім того, є ще неявна, але дуже важлива архітектурна відмінність.

Найчастіше виклик конструктора має якісь побічні ефекти, наприклад, впливає на документ. Якщо конструктор батька має якусь поведінку, яку потрібно перевизначити у нащадку, то у функціональному стилі це неможливо.

Інакше кажучи, у функціональному стилі в процесі створення Rabbit потрібно обов'язково викликати Animal.apply(this, arguments) , щоб отримати методи батька – і якщо цей Animal.apply, окрім додавання методів, каже: «Му-у-у!», то це проблема :

…Як немає в прототипному підході, тому що в процесі створення new Rabbit ми зовсім не зобов'язані викликати конструктор батька. Адже методи перебувають у прототипі.

Тому прототипний підхід варто віддавати перевагу функціональному як швидший і універсальніший. А що стосується краси синтаксису - вона дуже краща в новому стандарті ES6, яким можна користуватися вже зараз, якщо взяти транслятор babeljs.