The Complete Guide to JavaScript Classes
JavaScript использует прототипное наследование, то есть любой объект наследует свойства и методы своего объекта-прототипа. Традиционных классов, как шаблонов для создания объектов, которые используются в Java или Swift, в JavaScript не существует.
Напомним, что прототипный тип наследования имеет дело только с объектами. И прототипное наследование может лишь эмулировать классическое наследование классов. Для того, чтобы наконец реализовать традиционные классы в JavaScript, стандарт ES2015 ввёл синтаксис класса, однако он по сути является своеобразным синтаксическим сахаром над прототипным наследованием.
Этот пост познакомит вас с классами JavaScript: как определить класс, инициализировать новый экземпляр класса, определить его поля и методы, дадим понятие о реализации приватных и публичных полей, а также статических полей и методов.
Содержание
User
. Фигурные скобки { }
отделяют код с телом класса. Обратите внимание, что этот синтаксис называется объявлением класса (class declaration
class User { }
выражение класса (class expression
const UserClass = class { };
Так же вы можете легко экспортировать класс как часть модуля ES2015.
default export
export default class User { }
named export
export class User { }
Использование класса становится действительно полезным, если вы можете создавать экземпляры класса. Экземпляр — это объект, содержащий данные и поведение, описанные классом.
В языке JavaScript оператор new
создает экземпляр класса с использованием синтаксиса вида:
let instance = new Class()
Например, вы можете создать экземпляр класса User
с помощью оператора new
следующим образом:
const myUser = new User();
Конструктор constructor(param1, param2, ...)
— это специальный метод определяемый классом, который инициализирует его экземпляр. И это то, самое место в вашем коде, где вы можете установить любые начальные значения для полей экземпляра класса или можете выполнить любую настройку его свойств.
В следующем примере конструктор устанавливает начальное значение для поля name
:
class User{ constructor(name){ this.name = name; } }
Конструктор класса User
принимает единственный параметр name
this.name
.
Внутри конструктора значение ключевого слова this
эквивалентно вновь созданному экземпляру (а точнее представляет собой ссылку на него).
Аргументы, используемые для создания экземпляра класса, являются параметрами его конструктора:
class User { constructor(name) { name; this.name = name; // => 'Jon Snow' } } const user = new User('Jon Snow');
Параметр name
внутри конструктора получает значение Jon Snow
.
Если вы не определяете конструктор для класса, то создается конструктор по умолчанию. Конструктор по умолчанию является пустой функцией, которая при создании экземпляра никак его не изменяет.
В то же время класс JavaScript может иметь только один конструктор.
3. Поля
Поля класса являются его внутренними переменными, которые содержат его внутреннюю информацию. Поля могут быть привязаны к сущностям двух видов:
- Поля собственно экземпляра класса.
- Поля в объявлении класса (то есть статические поля).
Также поля имеют два уровня доступности:
- Публичное поле: поле доступное везде.
- Приватное поле: поле доступное только внутри тела класса.
3.1 Публичные поля экземпляра класса
class User { constructor(name) { this.name = name; } }
Выражение this.name = name
создает поле в экземпляре класса и присваивает ему начальное значение.
name
с помощью метода доступа к свойству:
const user = new User('Jon Snow'); user.name; // => 'Jon Snow'
name
является публичным public field и поэтому вы можете получить к нему доступ вне тела класса User
.
Когда поля создаются неявно внутри конструктора, как в предыдущем случае, то может быть трудно определить весь список полей, содержащихся в классе. Вы можете сделать только исходя из содержимого кода конструктора.
Наилучшим подходом является явное объявление полей класса. Независимо от того, что делает конструктор, экземпляр всегда имеет одинаковый набор полей.
Метод предложения полей класса class fields proposal позволяет очень наглядно определять поля внутри тела класса. Кроме того, вы можете сразу указать начальное значение:
class SomeClass { field1; field2 = 'Initial value'; // ... }
Давайте изменим наш класс User
и объявим публичное поле name
:
class User { name; constructor(name) { this.name = name; } } const user = new User('Jon Snow'); user.name; // => 'Jon Snow'
Инструкция name;
внутри тела класса объявляет name
публичное поле.
Объявление публичных полей таким образом очень наглядно: достаточно быстрого просмотра объявлений полей, чтобы понять структуру данных класса.
Более того, поле класса может быть инициализировано сразу при его объявлении.
class User { name = 'Unknown'; constructor() { // инициализации данных нет } } const user = new User(); user.name; // => 'Unknown'
Инструкция name = 'Unknown'
помещенная внутри тела класса, объявляет поле name
и инициализирует его значением 'Unknown'
.
Инкапсуляция — это важная концепция, которая позволяет скрыть внутренние детали реализации класса. Тот, кто в последствии будет использовать инкапсулированный класс при разработке своего приложения, может взаимодействовать только с открытым интерфейсом, который предоставляет ваш класс, и никак не связан с его деталями реализации.
Основным достоинством инкапсуляции является то, что классы, разработанные с ее использованием, легче обновлять при изменении деталей реализации.
Использовать приватные поля — хороший способ скрыть внутренние данные вашего объекта. Напомню, что эти поля могут быть прочитаны или изменены только в пределах класса, к которому они принадлежат. Вне класса вы никак не можете напрямую изменять значения приватных полей.
Приватные поля private fields доступны только внутри тела класса.
Внимание! Эта новая возможность была добавлена в язык недавно. В движках Javascript пока поддерживается частично и поэтому для её использования нужен соответствующий полифил.
Добавьте префикс, представляющий собой специальный символ #
, к имени поля для того, чтобы сделать его приватным, например #myField
. При этом префикс #
должен использоваться каждый раз, когда вы обращаетесь к приватному полю: объявляете его, читаете или изменяете его значение.
Теперь давайте убедимся в том, что нашему полю #name
мы можем присвоить значение лишь один раз: при инициализации экземпляра класса.
class User { #name; constructor(name) { this.#name = name; } getName() { return this.#name; } } const user = new User('Jon Snow'); user.getName(); // => 'Jon Snow' user.#name; // получаем исключение SyntaxError
#name
является приватным (в различной литературе могут так же использоваться другие термины, по сути являющиеся одним и тем же: скрытые или закрытые поля). Вы можете получить доступ или изменить #name
только внутри класса User
. При этом метод getName()
(подробнее о методах поговорим в следующем разделе), реализованный в нашем классе, имеет полный доступ к полю #name
.
Однако, если вы попытаетесь получить доступ к закрытому поле #name
вне нашего класса User
, то генерируется исключение с типом ошибка синтаксиса: SyntaxError: Private field '#name' must be declared in an enclosing class
.
3.3 Публичные статические поля
Внутри класса вы можете определять статические поля static fields. Этот тип полей полезен для определения констант класса или хранения информации, специфичной для него.
Для создания статических полей в классе JavaScript, используется специальное ключевое слово , static
за которой следует имя поля: static myStaticField
.
Давайте добавим новое поле, type
которое будет указывать на тип пользователя сайта: администратор или обычный (гость). Статические поля TYPE_ADMIN
и TYPE_REGULAR
удобно использовать в качестве констант для обозначения типа пользователя следующим образом:
class User { static TYPE_ADMIN = 'admin'; statiс TYPE_REGULAR = 'regular'; name; type; constructor(name, type) { this.name = name; this.type = type; } } const admin = new User('Site Admin', User.TYPE_ADMIN); admin.type === User.TYPE_ADMIN; // => true
User
определяем статические поля static TYPE_ADMIN
и static TYPE_REGULAR
. Теперь для получения доступа к их значениям, мы можем использовать имя класса, за которым после точки следует имя поля: User.TYPE_ADMIN
и User.TYPE_REGULAR
.
Использование значений статических полей класса возможно без его предварительной инициализации.
3.4 Приватные статические поля
Иногда даже статические поля могут стать деталями реализации вашего класса, которые вы хотели бы скрыть. Принимая это во внимание вы можете сделать статические поля приватными.
Чтобы сделать статическое поле класса приватным, добавьте к его имени префикс состоящий из специального символа #
, так же как мы поступали ранее с обычными приватными полями: static #myPrivateStaticField
.
Допустим, что вы хотите ограничить количество создаваемых экземпляров вашего класса User
. Чтобы скрыть информацию об ограничении их числа, вы можете использовать приватные статические поля:
class User { static #MAX_INSTANCES = 2; static #instances = 0; name; constructor(name) { User.#instances++; if (User.#instances > User.#MAX_INSTANCES) { throw new Error('Unable to create User instance'); } this.name = name; } } new User('Jon Snow'); new User('Arya Stark'); new User('Sansa Stark'); // возникает исключение Error
User.#MAX_INSTANCES
определяет максимальное количество разрешенных экземпляров, в то же время как статическое поле User.#Instances
сохраняет фактическое количество уже инициализированных экземпляров класса.
Приватные статические поля доступны только внутри класса User
. И ничто из внешнего мира не может помешать, только что рассмотренному нами, механизму ограничений: это еще одно преимущество использования концепции инкапсуляции в вашем коде.
4. Методы
Как нам уже известно поля класса содержат его данные. Возможность их модифицировать выполняют специальные функции, которые являются неотъемлемой его частью: методы methods.
Классы JavaScript поддерживают как создание и инициализацию начальными значениями их экземпляров, так и методы, которые обрабатывают значения их полей.
Например, давайте определим метод getName()
, который возвращает значение поля name
, уже знакомого нам по прошлым экспериментам, класса User
:
class User { name = 'Unknown'; constructor(name) { this.name = name; } getName() { return this.name; } } const user = new User('Jon Snow'); user.getName(); // => 'Jon Snow'
— это метод класса User
.
user.getName ()
— инструкция вызова метода. Она выполняет код метода и может возвращать некоторое вычисленное значение.
В методе класса, как и в конструкторе используется информация, которая хранится в текущем экземпляре для того, чтобы выполнить с ней какие-либо действия. Для этого используется ключевое слово this
, которое эквивалентно ссылке на текущий экземпляр класса. Используйте this
для доступа к данным полей внутри экземпляра класса: this.field
, а также для вызова других его методов внутри кода класса: this.method()
.
Давайте добавим к нашему классу User
новый метод nameContains(str)
, который будет принимать один параметр и вызывает другой его метод:
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } nameContains(str) { return this.getName().includes(str); } } const user = new User('Jon Snow'); user.nameContains('Jon'); // => true user.nameContains('Stark'); // => false
— это метод класса User
, который принимает параметр str
. Затем он выполняет другой метод текущего экземпляра this.getName()
, чтобы получить имя пользователя.
Как вы знаете методы также могут быть приватными и язык Javascript не исключение. Cделать метод приватным можно просто добавив к его имени префикс #
.
Давайте сделаем метод getName()
приватным:
class User { #name; constructor(name) { this.#name = name; } #getName() { return this.#name; } nameContains(str) { return this.#getName().includes(str); } } const user = new User('Jon Snow'); user.nameContains('Jon'); // => true user.nameContains('Stark'); // => false user.#getName(); // возбуждается исключение SyntaxError
#getName()
является приватным методом. Внутри метода nameContains(str)
вы вызываете приватный метод следующим образом: this.#GetName()
. Будучи закрытым, метод #getName()
не может быть вызван вне тела класса User
, поэтому выполнение кода в последней строке нашего примера вызовет возбуждение исключения SyntaxError
.
4.2 Геттеры (getters) и сеттеры (setters)
Как известно из общей теории разработки программного обеспечения, геттер и сеттер имитируют обычное поле класса, но с большим контролем разработчика над тем, как впоследствии содержимое поля будет доступно, а также каким образом модифицироваться.
Соответственно, геттер выполняется при попытке получить значение поля, а сеттер при попытке установить его новое значение.
Рассмотрим следующий пример. Для того, что бы быть уверенным, что свойство name
класса User
никогда не будет пустым, давайте подключим к приватному полю #nameValue
нашего класса специальные методы для получения и установки его значения (то есть определим для него геттер и сеттер):
class User { #nameValue; constructor(name) { this.name = name; } get name() { return this.#nameValue; } set name(name) { if (name === '') { throw new Error(`name field of User cannot be empty`); } this.#nameValue = name; } } const user = new User('Jon Snow'); user.name; // Вызывается геттер, => 'Jon Snow' user.name = 'Jon White'; // Вызывается сеттер user.name = ''; // Вызов сеттера с пустой строкой приводит к возбуждению исключения Error
get name() {...}
выполняется, когда вы хотите получить доступ к содержимому поля: . Когда выполняется сеттер set name(name) {...}
значение поля обновляется user.name = 'Jon White'
. Если новое значение для поля задается пустой строкой, то сеттер возвращает ошибку (возбуждается исключение).
4.3 Статические методы
Статические методы — это функции, прикрепленные непосредственно к классу, в котором они объявлены. И содержат логику, связанную с классом, а не с конкретным его экземпляром.
Для объявления статического метода используйте специальное ключевое слово static
, за которым следует обычный синтаксис метода: static myStaticMethod () {...}
.
При работе со статическими методами нужно помнить два простых правила:
- Статический метод может получить доступ к статическим полям.
- Статический метод не может получить доступ к полям экземпляра класса.
Например, давайте создадим статический метод, который определяет, был ли уже выбран пользователь с заданным именем.
class User { static #takenNames = []; static isNameTaken(name) { return User.#takenNames.includes(name); } name = 'Unknown'; constructor(name) { this.name = name; User.#takenNames.push(name); } } const user = new User('Jon Snow'); User.isNameTaken('Jon Snow'); // => true User.isNameTaken('Arya Stark'); // => false
— это статический метод, который использует статическое приватное поле для проверки уже принятых ранее имен.
Статические методы могут быть приватными: static #staticFunction(){...}
. Опять же следуя концепции инкапсуляции, вы можете вызывать приватный статический метод только внутри тела класса.
5. Наследование: extends
Классы в JavaScript поддерживают наследование одним только способом: с использованием ключевого слова extends
.
В выражении class Child extends Parent {...}
, класс Child
автоматически наследует от родительского класса Parent
его конструктор, поля и методы.
Например, давайте создадим новый дочерний класс ContentWriter
, который расширяет функционал нашего родительского класса User
.
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } class ContentWriter extends User { posts = []; } const writer = new ContentWriter('John Smith'); writer.name; // => 'John Smith' writer.getName(); // => 'John Smith' writer.posts; // => []
наследует от класса User
конструктор, метод getName()
, также его поле name
. В классе ContentWriter
мы объявляем новое поле, в котором будет находится массив сообщений posts
Обратите внимание, что приватные члены родительского класса не наследуются дочерним классом.
5.1 Конструктор родителя: super() в constructor()
Если вы захотите вызвать родительский конструктор в дочернем классе, то вам нужно использовать специальный метод super()
, доступный в конструкторе дочернего класса.
Например, давайте сделаем так, чтобы конструктор класса ContentWriter
вызывал родительский конструктор нашего класса User
, а затем инициализировал новое поле posts
:
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } class ContentWriter extends User { posts = []; constructor(name, posts) { super(name); this.posts = posts; } } const writer = new ContentWriter('John Smith', ['Why I like JS']); writer.name; // => 'John Smith' writer.posts // => ['Why I like JS']
super(name)
внутри дочернего класса ContentWriter
выполняется конструктор родительского класса User
. А за ним вы помещаете код вашего конструктора дочернего класса.
Обратите внимание на следующий ниже пример кода. Внутри конструктора дочернего класса мы сначала должны выполнить метод super()
перед использованием ключевого слова this
. Вызов метода super()
гарантирует то, что конструктор родительского класса заблаговременно инициализирует в экземпляре дочернего, поля и методы класса-родителя.
class Child extends Parent { constructor(value1, value2) { // Не работает! this.prop2 = value2; super(value1); } }
Для того, чтобы получить доступ к методу родительского класса внутри метода дочернего, вы должны использовать специальную ссылку на экземпляр родительского класса: super
.
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } class ContentWriter extends User { posts = []; constructor(name, posts) { super(name); this.posts = posts; } getName() { const name = super.getName(); if (name === '') { return 'Unknwon'; } return name; } } const writer = new ContentWriter('', ['Why I like JS']); writer.getName(); // => 'Unknwon'
getName()
дочернего класса ContentWriter
обращается к методу super.getName()
, который был реализован в родительского классе User
. То есть в дочернем классе мы заменяем родительский метод getName()
на свой с тем же именем. Этот механизм объектно-ориентированного программирования называется переопределением метода .
Обратите внимание, вы можете использовать ссылку super
со статическими методами, то есть получаете доступ к статическим методам родителя.
6. Проверка типа объекта: instanceof
Инструкция вида object instanceof Class
определяет, является ли object
экземпляром Class
.
Давайте посмотрим на оператор instanceof
в действии:
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } const user = new User('Jon Snow'); const obj = {}; user instanceof User; // => true obj instanceof User; // => false
user
является экземпляром класса User
, поэтому результат выполнения инструкции user instanceof User
определяется как true
.
Пустой объект {}
не является экземпляром User
, соответственно , obj instanceof User
возвращает false
.
Оператор instanceof
является полиморфным: он определяет экземпляр дочернего класса как экземпляр родительского.
class User { name; constructor(name) { this.name = name; } getName() { return this.name; } } class ContentWriter extends User { posts = []; constructor(name, posts) { super(name); this.posts = posts; } } const writer = new ContentWriter('John Smith', ['Why I like JS']); writer instanceof ContentWriter; // => true writer instanceof User; // => true
writer
является экземпляром дочернего класса ContentWriter
. Поэтому инструкция writer instanceof ContentWriter
возвращает true
.
В то же время ContentWriter
это дочерний класс User
. Поэтому writer instanceof User
true
.
Как же быть если вы хотите определить точно экземпляром какого класса является ваш объект? Для этого вы можете использовать свойство constructor
вашего экземпляра класса, значение которого вы сравнивается с именем класса:
writer.constructor === ContentWriter; // => true writer.constructor === User; // => false
7. Классы и прототипы
Как мы могли уже убедиться, синтаксис класса в JavaScript отлично справляется с абстрагированием от прототипного наследования. Для описания синтаксиса определения классов с использованием ключевого слова class
я даже не использовал термин прототип.
Но тем не менее, как нам известно, классы в Javascript все таки построены на основе прототипного наследования. То есть каждый класс по сути является функцией и создает его экземпляр при вызове в качестве конструктора.
Следующие два фрагмента кода эквивалентны.
Версия класса с использованием ключевого слова class
:
class User { constructor(name) { this.name = name; } getName() { return this.name; } } const user = new User('John'); user.getName(); // => 'John Snow' user instanceof User; // => true
Версия кода выше, переписанная с использованием прототипного наследования:
function User(name) { this.name = name; } User.prototype.getName = function() { return this.name; } const user = new User('John'); user.getName(); // => 'John Snow' user instanceof User; // => true
наследовании прототипов.
8. Доступность использования возможностей классов
Новые возможности использования классов, представленные в этом посте, отражены в стандарте ES2015 и предложениям, закладываемым в него на 3 этапе.
В конце 2019 года все, рассмотренные нами в посте, функциональные возможности классов представлены:
- Публичные и приватные поля экземпляра класса являются частью Class fields proposal;
- Приватные методы экземпляра, а также средства доступа (геттеры и сеттеры) являются частью Class private methods proposal;
- Публичные и приватные статические поля, а также приватные статические методы являются частью Class static features proposal;
- Все остальное является частью стандарта ES2015.
9. Выводы
Классы JavaScript инициализируют свои экземпляры конструкторами, определяя их методы, поля, а также их начальные значения. Вы можете прикрепить поля и методы к объявлению класса, используя ключевое слово static
.
Механизм наследования реализуется с использованием ключевого слова extends
, с помощью которого вы можете легко создать дочерний класс от родительского. Ключевое слово super
используется для доступа к полям и методам родительского класса из дочернего.
Для того, чтобы воспользоваться преимуществами концепции инкапсуляции, сделайте поля и методы приватными, чтобы скрыть внутренние детали реализации ваших классов. Имена приватных полей и методов должны начинаться с префикса #
.
Тем не менее классы в JavaScript становятся все более удобными в использовании. И в первую очередь это достигается повышением наглядности вашего кода для понимания его работы в случае командной разработки больших приложений.
А что вы думаете об использовании префикса #
при определении приватных свойств классов?