Всё, что вы хотели знать о классах в 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(); // Johnuser.#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'); // выведет trueuser.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'); // выведет trueuser.nameContains('Jane'); // выведет falseuser.#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; // срабатывает геттер и выводит Johnuser.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'); // выведет trueUser.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 Doeconsole.log(superUser.getName()) // John Doeconsole.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
, нам доступны приватные и статичные поля и методы. А это всё не может не радовать 😎.