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

7 хв. читання
02 листопада 2018

Ви, мабуть, вже чули щось на зразок: «У JavaScript усе є об'єктом».

Якщо ви ще не мали справ з об'єктами та не знаєте, які вони на вигляд, ось приклад:

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

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

У JavaScript існує два основних типи значень:

  • Примітиви: рядки, числа, булеві значення, undefined та null.
  • Об'єкти: масиви, функції, дати.

Отже, ми з'ясували, що у JavaScript не все є об'єктом. Однак необ'єктні речі огортаються в об'єкти під час компіляції, дозволяючи JavaScript здійснювати справді складні операції. Навіть якщо ви думаєте, що значення у вашому коді не є об'єктом, то, найімовірніше, JavaScript вже огорнув його.

Наприклад, якщо ми створимо порожню функцію під назвою hero:

function hero() {}

Як вже зазначалось, функції також розглядаються у JavaScript як об'єкти. Якщо ми додамо властивість type до цієї функції та виведемо результат виконання, то побачимо, що функція hero є об'єктом.

function hero() {}
hero.type = 'superman'
console.log(hero)
//Результат
{ [Function: hero] type: 'superman' }

Це одна з багатьох причин, чому JavaScript такий чудовий 🙌.

Аби зрозуміти, що таке Прототипне Наслідування, найперше треба розібратись, що таке об'єктноорієнтована мова програмування.

Флешбек — Об'єктноорієнтоване програмування

В ООП об'єкти використовують методи та властивості для взаємодії один з одним та створення складних застосунків. Такий підхід дозволяє розробникам з легкістю зберігати дані у структурованому та чистому вигляді.

У наведеному вище прикладі ми лише зберігали деякі дані. Якщо я хочу створити інший об'єкт «Batman», доведеться створювати усе спочатку:

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

У реальних застосунках такий підхід призводить до надлишкової повторюваності, це втомлює.

Можна побачити, що обидва об'єкти зберігають однакові дані: name, alias та planet. А якщо ми створимо загальний об'єкт, який використаємо для створення його окремих екземплярів?

Якщо ви знайомі з іншими мовами програмування, то вже здогадалися, що я маю на увазі створення класу. Але JavaScript базується на прототипах, а не класах.

У JavaScript загальний об'єкт відомий як Конструктор або Прототип.

JavaScript розглядає звичайну функцію та конструктор як одне й те саме. Але щоб розробникам було легше розрізняти їх, назви конструкторів пишуть з великої літери.

Прототип дозволяє створювати його унікальні екземпляри. Такий підхід використовується для чистоти та лаконічності коду.

Базуючись на вже створених об'єктах, я можу створити Прототип. Почнемо зі створення нової функції Hero. Важливо, щоб перша літера назви була великою.

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

Тепер я можу використовувати прототип для створення об'єктів superman та batman, застосовуючи ключове слово new. Ми використовуємо прототипну функцію для «конструювання» нового об'єкту — саме тому прототипи відомі також як конструктори.

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

Наслідування

Наслідування — процес, що дозволяє одному об'єкту базуватися на іншому. Такий підхід дозволяє об'єктам розділяти спільні властивості.

Раніше ми вже створили прототип Hero та використали його для створення нового об'єкта superman. Але ми не робили нічого з цим об'єктом. Тому подбаємо про це, створивши іншу функцію під назвою dialogue.

function dialogue() {
 console.log('I am ' + this.name);
}

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

Я хочу, щоб нова функція мала такі ж властивості, як і Hero. Замість прописувати їх у тілі функції, ми можемо просто вказати JavaScript, що необхідно наслідувати згадані властивості з прототипу Hero.

Згадане цілком можливо втілити за допомогою властивості prototype, що доступна у будь-якому JavaScript-об'єкті.

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

Розмістивши dialogue після Hero.prototype, ми зробили її доступною для усіх екземплярів Hero.

Диференційоване наслідування

JavaScript також підтримує іншу модель наслідування — диференційоване наслідування. У цій моделі методи не копіюються від «батька» до «нащадка». Натомість існує посилання між батьківським та дочірнім об'єктом.

superman насправді не має власного методу під назвою dialogue(). Але як тоді спрацьовує superman.dialogue()?

Коли рушій JavaScript знаходить у коді рядок superman.dialogue(), він шукає властивість dialogue всередині об'єкта superman. Коли він не бачить таку, то починає шукати прототип, що пов'язує об'єкт superman з його батьківським прототипом Hero.prototype. Тоді він знаходить Hero.prototype.dialogue. Далі здійснюється виклик знайденої функції через this, що прив'язує функцію до superman.

Object.create()

Все можна зробити більш зрозуміло, якщо створити новий клас для Superman, який успадкує властивості прототипу Hero. Такого ефекту можна досягнути присвоєнням прототипу Superman до прототипу Hero, ось так:

function Superman() {}
Superman.prototype = Hero.prototype

Але цим виразом ми лише прирівняли Superman та Hero. Нам необхідно створити новий об'єкт, що базується на прототипі Hero. З ES5 у JavaScript з'явилася вбудована функція Object.create(). Використаємо її так:

Superman.prototype = Object.create(Hero.prototype);

Створюємо новий порожній об'єкт, що базується на прототипі Hero, та присвоюємо його прототипу Superman. Так усі властивості, які ми мали у прототипі Hero, тепер можна отримати у прототипі Superman. Тому замість виклику new Hero, ми можемо викликати new Superman — і все працюватиме як слід.

Але якщо ви подивитесь на результат ближче, то помітите, що там є undefined. Так виходить, тому що наразі Hero є конструктором лише для самого себе. Нам треба здійснити call властивостей прототипу Hero всередині прототипу Superman.

function Superman() {
 Hero.call(this, 'Superman', 'Clark Kent', 'Krypton')
}

Створимо інший конструктор під назвою MarvelMovies:

function MarvelMovies(movieName, releaseYear) {
 this.movieName = movieName;
 this.releaseYear = releaseYear;
}

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

var avengers = new MarvelMovies("avengers", 2012);

Створимо новий метод під назвою output для прототипу:

MarvelMovies.prototype.output = function() {
 return "Movie: " + this.movieName + " Released in " + this.releaseYear;
}
console.log(avengers.output());

Майбутнє наслідування

Дійсно чудовим аспектом наслідування є те, що JavaScript дозволяє модифікувати чи розширяти функції класу навіть після його оголошення.

JavaScript відшукає прототип при спробі доступу до властивостей об'єкта. Тож можна змінювати класи у рантаймі.

Для наочності створимо масив:

var numbers = [11, 22, 33, 44, 55];
Array.prototype.shuffle = function() {
 return this.sort(function() {
 return Math.round( Math.random() * 2) - 1;
});
};
console.log(numbers.shuffle());

Тут масив numbers існував до оголошення Array.prototype.shuffle. Але у JavaScript пошук властивостей йде прототипним ланцюжком. Нам вдається використати метод shuffle до масиву саме тому, що він є у ланцюжку Array.prototype.

Простіше кажучи, ми створили масив, а потім надали усім масивам доступ до нового методу.

Висновок

Існує багато ресурсів, що пояснюють принцип роботи прототипного наслідування у JavaScript. В офіційній документації JavaScript зазначено, що:

  • Наслідування у JavaScript реалізується єдиною конструкцією: objects. Кожен object має внутрішнє посилання на інший object, що є його прототипом (prototype). Цей прототипний об'єкт має прототип самого себе і так далі, поки не буде досягнуто null. null за визначенням не має прототипу та поводиться як фінальне посилання у ланцюжку прототипів.*

Таке визначення може здатися заплутаним та важким для розуміння. Саме тому більшість розробників переходять на класове наслідування замість прототипного.

Існує ще багато цікавого у прототипному наслідуванні. З кожним новим синтаксисом, що надається ES5, ES6, ES7, з'являються нові можливості. Наприклад, object.assign є чудовим способом удосконалити фабричні методи та водночас звести код до мінімуму. Декілька цікавих способів використання має також Object.create.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація