В об’єктно-орієнтованому програмуванні, клас – це спеціальна конструкція, яка використовується для групування пов’язаних змінних та функцій. При цьому, згідно з термінологією ООП, глобальні змінні класу (члени-змінні) називаються полями даних (також властивостями або атрибутами), а члени-функції називають методами класу.
На практиці ми часто повинні створювати багато об’єктів одного й того ж виду, наприклад користувачів, або товари чи що завгодно.
Як ми вже знаємо з розділу Конструктори, оператор "new", new function
може допомогти з цим.
Але в сучасному JavaScript існує більш просунута конструкція “клас”, яка вводить нові чудові функції, які корисні для об’єктно-орієнтованого програмування.
Синтаксис “class”
Базовий синтаксис:
class MyClass {
// методи класу
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
Потім використовуйте new MyClass()
, щоб створити новий об’єкт з усіма перерахованими методами.
Метод constructor()
викликається автоматично за допомогою new
, в ньому ми можемо ініціалізувати об’єкт.
Наприклад:
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// Використання:
let user = new User("Іван");
user.sayHi();
Коли new User("Іван")
викликається:
- Створюється новий об’єкт.
constructor
запускається з даним аргументом і присвоює його вthis.name
.
…Потім ми можемо викликати методи об’єкту, такі як user.sayHi()
.
Часта помилка розробників-початківців полягає в тому, що ставляться коми між методами класу, що призводить до синтаксичної помилки.
Синтаксис класів не слід плутати з літералами об’єктів. У межах класу не треба ставити кому.
Що таке клас?
Отже, чим насправді є class
? Він не є цілком новою сутністю мови програмування, як можна подумати.
Тож, розвіймо магію і подивімося, чим в дійсності є клас. Це допоможе в розумінні багатьох складних аспектів.
У JavaScript клас є своєрідною функцією.
Погляньте на це:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// доказ: User -- це функція
alert(typeof User); // function
Конструкція class User {...}
в дійсності робить наступне:
- Створює функцію, що називається
User
та стає результатом оголошення класу. Код цієї функції береться з методуconstructor
(вважається порожнім, якщо ми не написали такий метод). - Записує методи класу, такі як
sayHi
, доUser.prototype
.
Після того, як new User
створився, коли ми викликаємо його метод, він береться з прототипу, як описано в розділі F.prototype. Таким чином, об’єкт має доступ до методів класу.
Ми можемо проілюструвати результат оголошення class User
наступним чином:
Ось код, щоб проаналізувати це:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// клас -- це функція
alert(typeof User); // function
// ...або, точніше, метод конструктора
alert(User === User.prototype.constructor); // true
// Методи знаходяться в User.prototype, наприклад:
alert(User.prototype.sayHi); // код sayHi методу
// у прототипі існує рівно два методи
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
Не просто синтаксичний цукор
Іноді люди кажуть, що class
– це “синтаксичний цукор” (синтаксис, який призначений для того, щоб полегшити читання, але не додає нічого нового), тому що фактично ми могли б оголосити те саме без використання ключового слова class
:
// перепишемо клас User в чистих функціях
// 1. Створимо функцію-конструктор
function User(name) {
this.name = name;
}
// прототип функції має властивість "constructor" за замовчуванням,
// тому нам не потрібно її створювати
// 2. Додамо метод до прототипу
User.prototype.sayHi = function() {
alert(this.name);
};
// Використання:
let user = new User("Іван");
user.sayHi();
Результат цього оголошення дуже схожий. Отже, дійсно існують причини, чому class
можна вважати синтаксичним цукром для того, щоб визначити конструктор разом із його методами прототипу.
Проте, існують важливі відмінності.
-
По-перше, функція, що створена за допомогою
class
, позначена спеціальною внутрішньою властивістю[[IsClassConstructor]]: true
. Так що це не зовсім те саме, що створити її вручну.Рушій перевіряє цю властивість у різних місцях. Наприклад, на відміну від звичайної функції, її треба викликати з
new
:class User { constructor() {} } alert(typeof User); // function User(); // Помилка: Конструктор класу User неможливо викликати без 'new'
Також, рядкове представлення конструктора класу у більшості рушіїв JavaScript починається з “class…”
class User { constructor() {} } alert(User); // class User { ... }
Є й інші відмінності, ми побачимо їх найближчим часом.
-
Методи класу неперелічувані. Оголошення класу встановлює прапор
enumerable
уfalse
для всіх методів в"prototype"
.Це добре тому, що коли ми перебираємо об’єкт за допомогою
for..in
, ми зазвичай не хочемо обробляти методи класу. -
Клас завжди
use strict
. Весь код всередині конструкції класу автоматично знаходиться в суворому режимі.
Крім того, синтаксис class
приносить багато інших особливостей, які ми дослідимо пізніше.
Class Expression
Так само, як функції, класи можуть бути визначені всередині іншого виразу, передані, повернуті, присвоєні тощо.
Ось приклад class expression:
let User = class {
sayHi() {
alert("Привіт");
}
};
Подібно до Named Function Expression, Class Expression може мати назву.
Якщо Class Expression має назву, то її видно лише всередині класу:
// "Named Class Expression"
// (немає такого терміну у специфікації, але це схоже на Named Function Expression)
let User = class MyClass {
sayHi() {
alert(MyClass); // назву MyClass видно тільки всередині класу
}
};
new User().sayHi(); // працює, показує визначення MyClass
alert(MyClass); // помилка, назву MyClass не видно за межами класу
Ми можемо навіть зробити класи динамічними “на вимогу”, наприклад:
function makeClass(phrase) {
// оголошуємо клас і повертаємо його
return class {
sayHi() {
alert(phrase);
}
};
}
// Створюємо новий клас
let User = makeClass("Привіт");
new User().sayHi(); // Привіт
Геттери/сеттери
Подібно до літералів об’єктів, класи можуть включати геттери/сеттери, обчислені атрибути тощо.
Ось приклад для user.name
, що реалізований за допомогою get/set
:
class User {
constructor(name) {
// викликає сеттер
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
alert("Ім’я занадто коротке.");
return;
}
this._name = value;
}
}
let user = new User("Іван");
alert(user.name); // Іван
user = new User(""); // Ім’я занадто коротке.
Технічно, таке оголошення класу працює шляхом створення геттерів та сеттерів в User.prototype
.
Обчислені назви […]
Ось приклад з обчисленою назвою методу за допомогою використання дужок [...]
:
class User {
['say' + 'Hi']() {
alert("Привіт");
}
}
new User().sayHi();
Такі особливості легко запам’ятати, оскільки вони нагадують літерали об’єктів.
Поля класу
Поля класу – це недавнє доповнення до мови.
Раніше наші класи мали лише методи.
“Поля класу” – це синтаксис, який дозволяє додавати будь-які властивості.
Наприклад, додаймо властивість name
до class User
:
class User {
name = "Іван";
sayHi() {
alert(`Привіт, ${this.name}!`);
}
}
new User().sayHi(); // Привіт, Іван!
Отже, ми просто пишемо <property name> = <value>
в оголошенні, і все.
Важливою відмінністю полів класу є те, що вони задаються в окремих об’єктах, а не в User.prototype
:
class User {
name = "Іван";
}
let user = new User();
alert(user.name); // Іван
alert(User.prototype.name); // undefined
Ми також можемо присвоїти значення, використовуючи складніші вирази та виклики функцій:
class User {
name = prompt("Ім’я, будь ласка?", "Іван");
}
let user = new User();
alert(user.name); // Іван
Створення методів, що пов’язані з полями класу
Як показано в розділі Прив’язка контексту до функції, функції в JavaScript мають динамічний this
. Він залежить від контексту виклику.
Отже, якщо метод об’єкта передається і викликається в іншому контексті, this
більше не буде посиланням на цей об’єкт.
Наприклад, цей код покаже undefined
:
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("привіт");
setTimeout(button.click, 1000); // undefined
Ця проблема називається “втратою this
”.
Існує два підходи до розв’язання цієї проблеми, як обговорювалося в розділі Прив’язка контексту до функції:
- Передати функцію-обгортку, наприклад
setTimeout(() => button.click(), 1000)
. - Зв’язати метод з об’єктом за допомогою функції
bind
, наприклад у конструкторі.
Поля класу надають інший, досить елегантний синтаксис:
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("привіт");
setTimeout(button.click, 1000); // привіт
Поле класу click = () => {...}
створюється на основі конкретного об’єкта, існує окрема функція для кожного об’єкта Button
, з this
всередині неї, що посилається на цей об’єкт. Ми можемо передати кнопку куди завгодно, а значення this
завжди буде коректним.
Це особливо корисно в середовищі браузера, для слухачів подій.
Підсумки
Основний синтаксис класу виглядає так:
class MyClass {
prop = value; // властивість
constructor(...) { // конструктор
// ...
}
method(...) {} // метод
get something(...) {} // геттер метод
set something(...) {} // сеттер метод
[Symbol.iterator]() {} // метод з обчисленим ім’ям (символом в цьому випадку)
// ...
}
MyClass
технічно є функцією (тою, яку ми задаємо як constructor
), тоді як методи, геттери та сеттери записуються до MyClass.prototype
.
У наступних розділах ми дізнаємося більше про класи, включаючи наслідування та інші особливості.