Класи в JavaScript
Клас у JavaScript це синтаксичний цукор над функцією-конструктором і прототипами. Ззовні це декларативний опис типу об'єкта, а під капотом усе працює так само, як і раніше: методи лягають у prototype, а new створює новий об'єкт і прив'язує до нього this. Виглядає й читається такий код значно простіше.
Теорія
TL;DR
classце синтаксичний цукор над функцією-конструктором і прототипами, а не нова модель успадкування.constructor()викликається приnew, а методи з тіла класу потрапляють уprototypeі є спільними для всіх екземплярів.extendsзв'язує прототипи двох класів,super()викликає конструктор батька.staticробить метод або властивість членом самого класу, а не екземпляра.- Поля з
#приватні й доступні лише всередині тіла класу (ES2022), аget/setкерують читанням і записом властивостей. - Класи не піднімаються (немає hoisting) як функції, а їхні методи не перелічувані (non-enumerable).
Швидкий приклад
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log(`Привіт, мене звати ${this.name}`);
}
}
const user1 = new User('Alex', 25);
user1.sayHi(); // Привіт, мене звати Alexclassоголошує клас.constructor()це спеціальний метод, який викликається приnew User().thisце посилання на об'єкт, що створюється.- Методи класу автоматично потрапляють у
prototype.
Як це виглядає під капотом
Цей код:
class User {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(this.name);
}
}еквівалентний старому ES5-коду:
function User(name) {
this.name = name;
}
User.prototype.sayHi = function() {
console.log(this.name);
};Різниця не в моделі, а в деталях поведінки: тіло класу завжди виконується у strict mode, методи класу не перелічувані, тому for...in по екземпляру їх не побачить, а викликати клас без new взагалі не можна.
Складові класу
| Можливість | Опис |
|---|---|
| constructor() | Метод, який виконується при створенні об'єкта (new) |
| методи класу | Додаються в prototype і доступні всім екземплярам |
| extends | Успадкування від іншого класу |
| super() | Виклик конструктора батька |
| static | Статичні методи та властивості, належать самому класу |
приватні поля (#) | Поля, недоступні поза класом |
| геттери та сеттери | Дозволяють керувати читанням і записом властивостей |
Клас можна оголосити не лише інструкцією, а й виразом (class expression):
const User = class {
sayHi() {
console.log('Hi!');
}
};
new User().sayHi(); // Hi!Або дати йому ім'я, видиме тільки всередині самого визначення:
const User = class MyUser {
getClassName() {
return MyUser.name;
}
};Успадкування: extends і super
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} видає звук.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // викликає constructor батька
this.breed = breed;
}
speak() {
console.log(`${this.name} гавкає!`);
}
}
const rex = new Dog('Rex', 'вівчарка');
rex.speak(); // Rex гавкає!extendsзв'язує прототипи класів.super()викликаєconstructorбатька, і в похідному класі його треба викликати до першого звернення доthis.- Методи можна перевизначати, як у класичному ООП.
static, приватні поля та аксесори
Статичні методи й властивості належать самому класу, а не його екземплярам:
class MathUtil {
static add(a, b) {
return a + b;
}
}
console.log(MathUtil.add(2, 3)); // 5Приватні поля та методи (#) додані в ES2022:
class Account {
#balance = 0; // приватне поле
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const acc = new Account();
acc.deposit(100);
console.log(acc.getBalance()); // 100
console.log(acc.#balance); // помилка: приватна властивістьПоля з # доступні лише всередині класу.
Геттери й сеттери дозволяють керувати тим, як властивість читається і змінюється:
class User {
constructor(name) {
this._name = name;
}
get name() {
return this._name.toUpperCase();
}
set name(value) {
this._name = value.trim();
}
}
const user = new User(' tim ');
console.log(user.name); // TIM
user.name = ' Alex ';
console.log(user.name); // ALEXКлас проти функції-конструктора
| Критерій | Функція-конструктор | Клас |
|---|---|---|
| Синтаксис | Старий, громіздкий | Сучасний, декларативний |
new обов'язковий | Так | Так |
| Методи | Через prototype | Усередині тіла класу |
Поведінка this | Може бути непередбачуваною | Зрозуміла, стандартна |
Підтримка extends | Вручну | Вбудована |
| Приватні поля | Немає | Є (через #) |
Коротке резюме синтаксису:
| Поняття | Що робить |
|---|---|
class | Оголошує клас |
constructor | Метод, що викликається при new |
extends | Успадкування |
super() | Виклик батьківського конструктора |
static | Методи класу, а не екземпляра |
#field | Приватна властивість |
get / set | Керування доступом до властивостей |
Типові помилки
-
Думати, що класи піднімаються, як функції. Оголошення класу потрапляє в temporal dead zone, тому звернення до нього вище за рядок з
classпадає:javascriptconst user = new User(); // ReferenceError class User {} -
Викликати клас без
new.User('Alex')даєTypeError: Class constructor User cannot be invoked without 'new', тоді як стара функція-конструктор у такому виклику мовчки псувала глобальний об'єкт. -
Звертатися до
thisдо викликуsuper(). У похідному класіthisще не існує, доки не відпрацював конструктор батька, тому будеReferenceError. -
Чекати на методи класу у
for...in. Вони не перелічувані, на відміну від властивостей, дописаних у прототип вручну. -
Передавати метод як колбек і втрачати
this.setTimeout(user.sayHi, 0)викличе метод без прив'язки; потрібніuser.sayHi.bind(user), стрілкова обгортка або поле класу зі стрілковою функцією. -
Плутати
#balanceз угодою про іменування_balance. Підкреслення це лише домовленість, а поле з#справді недоступне ззовні: звернення до нього поза тілом класу є синтаксичною помилкою.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.