Всё, что вы хотели знать о классах в Javascript
В javascript, в отличие от ООП-языков (как, например, PHP), нет реализации классов. Таким образом «классы» в javascript это лишь абстракция, надстройка над прототипным наследованием, которая лишь иммитирует классы. С приходом ES2015 делать эту иммитацию стало немного проще, так как теперь у нас есть синтаксический сахар в виде Class. Вот и поглядим, что можно с ним делать.
Создание класса
Итак, для инициализации класса достаточно написать:
class User {
// Тело класса
};
Можно поместить его в переменную:
const User = class {
// Тело класса
};
Можно написать и так:
export default class USer {
// Тело класса
};
А можно использовать именованный экспорт:
export class User {
// Тело класса
};
Тут никаких ограничений, делайте как вам удобно. Единственное, о чём нужно помнить — рекомендуется, как правило хорошего тона, имена классов писать с заглавной буквы.
Но объявить класс это только полдела. Его же нужно как-то использовать. Для создания экземпляра (объекта, содержащего в себе все данные и методы) класса используем оператор new
:
const user = new User();
Инициализация экземпляра класса
При инициализации экземпляра класса срабатывает специальная функция — constructor
, которую мы описываем в самом классе:
class User() {
constructor(name) {
this.name = name;
}
};
В данном примере функция-конструктор принимает аргумент name
, который мы используем для определения свойства name
класса. Аргумент мы передаем при создании экземпляра класса:
const user = new User("John");
Внутри конструктора this
ссылается на свежесозданый экземпляр класса.
Если мы не опишем конструктор в своём классе, то функция constructor
всё равно будет создана, просто она будет пустой.
Свойства(поля) класса
Так как экземпляр класса в javascript является объектом, то логичнее говорить о его свойствах. Но, когда речь идёт о классах, принято говорить о его полях. Так что отключим режим зануды и будем говорить не свойства, а поля.
Итак, в классе мы можем иметь два вида полей:
- Поля экземпляра класса;
- Поля самого класса (статичные).
Каждый из этих видов может, в свою очередь, принадлежать к одному из двух типов:
- Публичный, т.е. доступный извне;
- Приватный, т.е. доступный только внутри класса.
Публичные поля экземпляра класса
С ними мы уже знакомы. Мы выше уже писали:
class User() {
constructor(name) {
this.name = name;
}
};
const user = new User("John");
Здесь мы создали экземпляр класса User
. У этого класса есть поле name
. И мы легко можем получить к нему доступ:
console.log(user.name); // John
Из этого, как вы уже догадались, следует, что name
это публичное поле экземпляра класса. Для большего удобства мы можем декларировать все поля не в конструкторе, а в теле класса. Это особенно удобно, если мы хотим присвоить полям дефолтные значения, чтобы сделать необязательной передачу аргументов в конструктор. Также это облегчает понимание структуры класса.
class User() {
name = "John";
surname;
constructor(surname) {
this.surname = surname
}
}
Вот, например, класс, все экземпляры которого будут иметь имя John, а фамилию будут получать из аргументов. И нам достаточно одного взгляда, чтобы сразу увидеть, все публичные поля экземпляра.
Публичные поля мы можем без всяких ограничений читать и модифицировать в конструкторе, в методах класса и извне класса.
Приватные поля экземпляра класса
Приватные поля, как понятно из их названия, доступны не всем. Доступ к ним имеется только в теле класса. Чтобы обозначить приватное поле перед его именем ставится символ #
.
class User {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const user = new User("John");
user.getName(); // John
user.#name; // SyntaxError: Private field '#name' must be declared in an enclosing class
Вот в нашем классе мы объявили приватное поле name
. У метода getName
в теле класса есть доступ к этому полю. А вот получить его напрямую не получится. На то оно и приватное.
Публичные статичные поля
Статичные поля — это поля, которые принадлежат самому классу, а не его экземпляру. В них удобно хранить какие-то константы или информацию, которая актуальна для всех без исключения экземпляров класса. Давайте добавим пару таких полей нашему классу User
.
class User {
static TYPE_ADMIN = "admin";
static TYPE_REGULAR = "regular";
name;
type;
constructor(name, type) {
this.name = name;
this.type = type;
}
}
const admin = new User("Adam Smith", User.TYPE_ADMIN);
admin.type === User.TYPE_ADMIN // true
Мы создали две публичные статичные переменные: TYPE_ADMIN
и TYPE_REGULAR
. Поскольку статичные переменные принадлежат самому классу, то для обращения к ним мы используем имя класса: User.TYPE_ADMIN
. Попытка обратиться к ним через экземпляр класса: admin.TYPE_ADMIN
– вернёт нам undefined
.
Приватные статичные поля
Ну здесь всё так же, как и в полях экземпляра класса – перед именем добавляем #
:
class User {
static #MAX_INSTANCES = 2;
static #instances = 0;
constructor(name) {
User.#instances += 1;
if (User.#instances > User.#MAX_INSTANCES) {
throw new Error("Too many users!")
}
this.name = name;
}
}
new User("John");
new User ("Adam");
new User ("Robert") // Вернёт ошибку
Методы класса
Итак, с полями разобрались. Двигаемся дальше. В полях у нас хранится информация. Но сама по себе она мало интересна. Она нам нужна для, того, чтобы что-то с ней делать. И вот как раз для этого и предназначены методы. Т.е. это просто функции, которые выполняют какие-то действия с имеющейся информацией. Как и поля, методы делятся на два типа: методы экземпляра класса и статичные.
Методы экземпляра класса
Методы экземпляра класса имеют доступ к данным класса и могут вызывать другие методы.
class User {
name = 'Unknown';
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
const user = new User('John');
user.getName(); // выведет John
В примере выше у нас есть класс User, у которого есть метод getName. Как и в функции constructor
this
внутри метода ссылается на созданный экземпляр класса. Давайте добавим ещё один метод, который будет вызывать другой метод.
class User {
name = 'Unknown';
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
nameContains(string) {
return this.getName().includes(string);
}
}
const user = new User('John');
user.nameContains('John'); // выведет true
user.nameContains('Jane'); // выведет false
Как и поля, методы могут быть приватными. Давайте сделаем метод getName
приватным.
class User() {
#name = 'Unknown';
constructor(name) {
this.name = name;
}
#getName() {
return this.#name;
}
nameContains(string) {
return this.#getName().includes(string);
}
}
const user = new User('John');
user.nameContains('John'); // выведет true
user.nameContains('Jane'); // выведет false
user.#getName(); // выведет ошибку
Геттеры и сеттеры
Геттеры и сеттеры это специальные методы, которые срабатывают автоматически, когда вы обращаетесь к какому-то полю, чтобы получить(геттеры) или изменить(сеттеры) его значение.
class User {
#nameValue;
constructor(name) {
this.name = name;
}
get name() {
return this.#nameValue
}
set name(name) {
if (name === '') {
throw new Error('Name field can not be empty.')
}
this.#nameValue = name;
}
}
const user = new User('John');
user.name; // срабатывает геттер и выводит John
user.name = 'Jane'; // срабатывает сеттер и user.name теперь Jane
Статичные методы
Статичные методы, как и статичные поля, принадлежат самому классу, а не его экземпляру. Соответственно, в этих методах должна быть логика, которая актуальна абсолютно для всех будущих экземпляров класса.
При работе со статичными методами важно помнить два правила:
- Статичные методы имеют доступ к статичным полям,
- Статичные методы не имеют доступа к полям экземпляра класса.
Давайте создадим метод, который проверяет есть ли уже пользователь с указанным именем.
class User {
static #takenNames = [];
static isNameTaken(name) {
return User.#takenNames.includes(name);
}
name = 'Unknown';
constructor(name) {
if (User.isNameTaken(name)) {
throw new Error('This name is already taken');
}
this.name = name;
User.#takenNames.push(name);
}
}
const user = new User('John');
User.isNameTaken('John'); // выведет true
User.isNameTaken('Jane'); // выведет false
Статичные методы могут быть приватными. В этом случае на них распространяются те же, правила, что и на статичные приватные поля.
extends
Наследование: Классы в javascript поддерживают наследование с помощью ключевого слова extends
. Запись class Child extend Parent {...}
говорит о том, что класс Child наследует от класса Parent функцию-конструктор, поля и методы.
Рассмотрим пример, создав класс Admin, который будет наследником класса User.
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Admin extends User {
status = 'admin';
}
const superUser = new Admin('John Doe');
console.log(superUser.name) // John Doe
console.log(superUser.getName()) // John Doe
console.log(superUser.status // admin
Как видите класс Admin унаследовал функцию-конструктор, метод getName()
и поле name
. Также у этого класса есть собственное поле status
.
Но важно помнить, что приватные поля и методы не наследуются.
super()
Родительский конструктор: При создании класса-наследника есть одна особенность. Дело в том, что после создания у него не будет собственного this
. И если вы попытаетесь в новом классе сделать что-то такое:
class Child extends Parent {
constructor(name) {
this.name = name;
}
}
то вы получите ошибку. И вот чтобы инициализировать this
для созданного класса, необходимо вызвать функцию super()
, которая в свою очередь вызовет конструктор класса-родителя, который выдаст классу-наследнику this
. Т.е. корректный код должен выглядеть как-то так:
class User {
name;
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Employee extends User {
status;
constructor(name, status) {
super(name);
this.status = status;
}
}
Заодно, благодаря вызову super()
, мы сразу видим какие поля принадлежат родителю, а какие ребёнку.
super
Родительский класс: Кроме функции super()
дочернему классу доступно также ключевое слово super
, которое позволяет вызвать метод родительского класса.
class User {
name;
status;
constructor(name, status) {
this.name = name;
this.status = status;
}
renderProperties(el) {
el.innerHTML = JSON.stringify(this);
}
}
Например, у нас есть класс User
. у него есть метод renderProperties
, который получает в качестве аргумента элемент Node-дерева и рендерит в него все поля класса. Теперь создадим дочерний класс.
class Worker extends User {
constructor {
super('John', 'Frontend developer');
}
renderWithSuper(el) {
el.classList.add('green');
super.renderProperties(el);
}
}
Мы создаём дочерний класс Worker
, которые наследуется от класса User
. И нам нужно, чтобы, дочерний класс рендерил текст зелёного цвета. Мы можем переопределить метод renderProperties
родительского класса. Но мы решили так не делать, а сделали новый метод renderWithSuper
, в котором совершаем манипуляции с элементом и с помощью super
вызываем родительский метод renderProperties
. Собственно это всё, что нужно знать о super
. Это ключевое слово позволяет вам вызвать метод родительского класса внутри дочернего класса.
Заключение
С появлением Class
работа с классами в javascript стала намного проще и приятнее. Да, это всего лишь синтаксический сахар. Но он позволяет нам писать более читаемый и лаконичный код. Нам доступно понятное наследование с помощью extends
, нам доступны приватные и статичные поля и методы. А это всё не может не радовать 😎.