Треки
/
JavaScript
JavaScript
/
Салабус
/
Прототипи та класи
Пр

Прототипи та класи у JavaScript

32 вправи

Про концепцію Прототипи та класи

JavaScript надає можливості для обʼєктно-орієнтованого програмування (ООП). В ООП ми створюємо обʼєкти (екземпляри) з «шаблонів» (класів), щоб вони мали певні дані та функціональність. Властивості з даними в контексті ООП називаються полями, а властивості-функції - методами.

У JavaScript класів не було взагалі, доки їх не додали до специфікації мови 2015 року, але обʼєктно-орієнтоване програмування було можливе завдяки наслідуванню на основі прототипів. І хоча сьогодні вже є ключове слово class, JavaScript усе ще залишається прототипною мовою.

Щоб зрозуміти, що означає бути прототипною мовою і як JavaScript насправді працює, повернімося в часи, коли класів не було.

Синтаксис прототипів

Функція-конструктор

У JavaScript роль шаблону (класу) виконує звичайна функція. Коли функція має використовуватися як такий шаблон, її називають функцією-конструктором, і за домовленістю її назва має починатися з великої літери. Екземпляри (обʼєкти) створюються з шаблону за допомогою ключового слова new під час виклику функції-конструктора.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

Важливо зауважити, що в JavaScript екземпляри й функція-конструктор зберігають звʼязок одне з одним навіть після створення екземплярів. Кожен обʼєкт-екземпляр має приховану внутрішню властивість, яку в специфікації мови позначено як [[prototype]]. Вона зберігає посилання на значення ключа prototype функції-конструктора. Так, ми не помилилися: функція в JavaScript може мати пари ключ-значення, бо за лаштунками вона теж є обʼєктом.

Починаючи з 2015 року до [[prototype]] можна дістатися через Object.getPrototypeOf(). До того в багатьох середовищах він був доступний через ключ __proto__.

Не плутаймо прототип обʼєкта ([[prototype]]) із властивістю prototype функції-конструктора.

Note

Підсумуймо:

  • Конструктори в JavaScript - це звичайні функції.
  • Створення нового екземпляра породжує обʼєкт, повʼязаний зі своїм конструктором; цей звʼязок називають його прототипом.
  • Функції - це обʼєкти (обʼєкти, які можна викликати), тому вони можуть мати властивості.
  • Властивість prototype функції-конструктора стає прототипом екземпляра.

Поля екземпляра

Часто нам потрібно, щоб усі похідні обʼєкти (екземпляри) мали певні поля, і щоб під час створення обʼєкта їм передавалися початкові значення. Цього можна досягти за допомогою ключового слова this. Усередині функції-конструктора this представляє новий обʼєкт, який буде створено через new. Коли функцію-конструктор викликають із new, this повертається автоматично.

Це означає, що ми можемо додати поля до нового екземпляра, додавши їх до this у функції-конструкторі.

function Car(color, weight) {
  this.color = color;
  this.weight = weight;
  this.engineRunning = false;
}

const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false

Методи екземпляра

Методи додаються через властивість prototype функції-конструктора. Усередині методу ми можемо звертатися до полів екземпляра через this. Це працює завдяки такому загальному правилу.

Коли функцію викликають як метод обʼєкта, її this вказує на обʼєкт, для якого викликано метод. 1

function Car() {
  this.engineRunning = false;
  // ...
}

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

Car.prototype.addGas = function (litre) {
  // ...
};

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

Ланцюжок прототипів

myCar із прикладу вище - звичайний обʼєкт JavaScript. Якби ми зазирнули до нього (наприклад, у консолі браузера), то не знайшли б усередині самого обʼєкта myCar властивості startEngine зі значенням-функцією. То як же тоді працює код вище?

Секрет тут називається ланцюжком прототипів. Коли ми намагаємося звернутися до будь-якої властивості (поля чи методу) обʼєкта, JavaScript спершу перевіряє, чи є відповідний ключ безпосередньо в самому обʼєкті. Якщо ні, він далі шукає цей ключ в обʼєкті, на який посилається властивість [[prototype]] вихідного обʼєкта. Як уже згадувалося, у нашому прикладі [[prototype]] вказує на властивість prototype функції-конструктора. Саме там JavaScript і знайде функцію startEngine, бо ми додали її туди.

function Car() {
  // ...
}

Car.prototype.startEngine = function () {
  // ...
};

І ланцюжок на цьому не закінчується. Властивість [[prototype]] обʼєкта Car.prototype (myCar.[[prototype]].[[prototype]]) посилається на Object.prototype (властивість prototype функції-конструктора Object). Він містить загальні методи, доступні всім обʼєктам JavaScript, наприклад toString(). [[prototype]] обʼєкта Object зазвичай дорівнює null, тож ланцюжок прототипів там і завершується. Отже, ми можемо викликати myCar.toString(), і цей метод існуватиме, бо JavaScript шукає його по всьому ланцюжку прототипів. Докладний приклад є в статті MDN «Inheritance and the prototype chain».

Caution

Зауважмо, що ланцюжком прототипів ми рухаємося лише тоді, коли читаємо значення. Пряме присвоєння чи видалення властивості обʼєкта-екземпляра стосується лише цього конкретного екземпляра. Це може виявитися несподіванкою, якщо ми звикли до мови з наслідуванням на основі класів.

Динамічні методи (додавання методів усім наявним екземплярам)

JavaScript дозволяє додавати методи всім наявним екземплярам навіть після їх створення.

Ми вже знаємо, що кожен екземпляр зберігає посилання на властивість prototype функції-конструктора. Це означає, що якщо додати запис до цього обʼєкта prototype, новий запис (наприклад, новий метод) одразу стане доступним усім екземплярам, створеним цією функцією-конструктором.

function Car() {
  this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.

Теоретично динамічні методи можна навіть використовувати, щоб розширити функціональність вбудованих обʼєктів на кшталт Object чи Array, змінивши їхній прототип. Це називається monkey patching. Оскільки ця зміна впливає на весь застосунок, її краще уникати, щоб не спричинити непередбачених побічних ефектів. Єдиний розумний випадок застосування - додати поліфіл для методу, якого бракує в старіших середовищах.

Синтаксис класів

Сьогодні JavaScript підтримує визначення класів за допомогою ключового слова class. Його додали до специфікації мови 2015 року. З одного боку, це синтаксичний цукор, який робить класи простішими для читання й написання. Новий синтаксис більше схожий на те, як класи пишуть у мовах на кшталт C++ чи Java. Розробникам, які переходять із цих мов, легше адаптуватися. З іншого боку, синтаксис класів прокладає шлях для нових можливостей мови, яких немає в синтаксисі прототипів.

Оголошення класів

У новому синтаксисі класи визначають ключовим словом class, після якого йдуть назва класу й тіло класу у фігурних дужках. Тіло містить визначення функції-конструктора, тобто спеціального методу з назвою constructor. Ця функція працює так само, як функція-конструктор у синтаксисі прототипів. Тіло класу також містить усі визначення методів. Синтаксис методів схожий на скорочений запис, який ми бачили для додавання функцій як значень усередину обʼєкта, дивіться концепт «Обʼєкти».

class Car {
  constructor(color, weight) {
    this.color = color;
    this.weight = weight;
    this.engineRunning = false;
  }

  startEngine() {
    this.engineRunning = true;
  }

  addGas(litre) {
    // ...
  }
}

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

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

Памʼятаймо, що за лаштунками JavaScript усе ще залишається прототипною мовою. Усі механізми, про які ми дізналися в розділі «Синтаксис прототипів» вище, досі працюють.

Приватні поля, гетери та сетери

Типово всі поля екземпляра в JavaScript є публічними. До них можна звертатися й присвоювати їм значення напряму.

Робота над додаванням справжніх приватних полів до специфікації мови ще триває, подробиці в документі-пропозиції.

Тим часом можна скористатися усталеною домовленістю: поля й методи, назви яких починаються з підкреслення, вважаються приватними. До них ніколи не слід звертатися напряму ззовні класу.

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

class Car {
  constructor() {
    this._mileage = 0;
  }

  get mileage() {
    return this._mileage;
  }

  set mileage(value) {
    throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
    // Just an example, usually you would not provide a setter in this case.
  }
}

const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.

Поля та методи класу

В ООП іноді потрібні допоміжні поля чи методи, які не залежать від конкретного екземпляра. Натомість вони визначені для самого класу. Цього можна досягти за допомогою ключового слова static.

class Car {
  static type = 'vehicle';

  static isType(targetType) {
    return targetType === 'vehicle';
  }
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false

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

Окрім наслідування вздовж ланцюжка прототипів, яке ми бачили раніше, у JavaScript можна також подати наслідування між класами. Про це йдеться в концепті «Наслідування».


  1. this Examples - As an object method, MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (дата звернення: 3 грудня 2021 р.) ↩

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Прототипи та класи