Menu

Классы в JavaScript: конструктор, методы и new

Разбираемся, как на самом деле устроены классы в JavaScript: конструктор, методы, свойства экземпляра, геттеры и сеттеры - и что скрывается за ключевым словом class.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Класс - это чертёж для объектов

Класс описывает, как устроен объект определённого типа и что он умеет делать. Вы один раз описываете «чертёж», а потом штампуете по нему столько экземпляров, сколько нужно. У каждого экземпляра свои данные, но методы - общие.

Базовый синтаксис класса в JavaScript выглядит так:

class User { ... } задаёт шаблон, а new User(...) создаёт экземпляр. У каждого экземпляра своё name и свой email, но метод greet один на всех - он живёт на самом классе.

Два экземпляра, два набора данных, один набор методов. В этом вся суть.

Конструктор класса в JavaScript инициализирует экземпляр

constructor - это особый метод класса, который вызывается ровно один раз при создании объекта через new. Его задача - подготовить новый объект, как правило, записав переданные аргументы в this:

this внутри конструктора - это та самая свежесозданная сущность, которую мы как раз собираем. Когда мы пишем this.x = x, мы кладём x именно на этот объект. Каждый вызов new Point(...) получает собственный this, а значит, и свои собственные x и y.

Если конструктор не описать самому, JavaScript подставит пустой за нас. Писать его вручную имеет смысл только тогда, когда при создании объекта реально нужно что-то настроить.

Ключевое слово new - сердце всей механики

Вызов класса без new выбрасывает ошибку:

const p = Point(3, 4);
// TypeError: Class constructor Point cannot be invoked without 'new'

Это сделано специально. Оператор new выполняет четыре действия по порядку:

  1. Создаёт новый пустой объект.
  2. Привязывает его к прототипу класса (где хранятся методы).
  3. Вызывает constructor, где this указывает на этот новый объект.
  4. Возвращает получившийся объект.

Без new ничего из этого не произойдёт. Класс требует его обязательно - и это большой шаг вперёд по сравнению с эпохой до class, когда забытый new мог незаметно испортить глобальный объект.

Методы - общие, поля класса - у каждого экземпляра свои

Методы, описанные в теле класса, живут на прототипе - одна копия на всех, общая для всех экземпляров. А поля экземпляра (всё, что мы присваиваем через this) создаются заново для каждого объекта - у каждого свои.

У каждого экземпляра своё count, поэтому увеличение счётчика в одном объекте не затрагивает другой. А вот a.increment и b.increment - это буквально одна и та же функция: она хранится в единственном экземпляре на прототипе и подтягивается при каждом вызове у конкретного объекта. Именно поэтому классы дёшево масштабируются: тысяча экземпляров не создаёт тысячу копий каждого метода.

Поля класса в JavaScript

Поля экземпляра можно объявлять прямо в теле класса, вне конструктора:

Объявления полей выполняются до тела конструктора - как если бы они были прописаны в самом начале constructor. Это удобно, когда начальное значение не зависит от аргументов конструктора: лучше уж так, чем забивать конструктор строчками вроде this.count = 0; this.step = 1;.

А вот если значение всё-таки зависит от аргументов, присваивание логичнее оставить внутри конструктора - там, где эти аргументы доступны.

Геттеры и сеттеры в JavaScript

Геттер или сеттер внешне похож на метод, но ведёт себя как обращение к свойству. Вы читаете или присваиваете значение без скобок, а под капотом срабатывает функция:

Обрати внимание на то, как всё это вызывается: t.fahrenheit (без скобок) - это обращение к геттеру, а t.fahrenheit = 100 срабатывает как сеттер. Снаружи fahrenheit выглядит как самое обычное свойство, но его значение вычисляется на лету из celsius.

Геттеры удобны для производных значений. Сеттеры - для валидации или нормализации при присваивании. Только не злоупотребляй ими: если геттер выполняет тяжёлую работу, читающий код человек будет неприятно удивлён тем, что «простое обращение к свойству» на самом деле что-то считает.

Краткая запись методов, вычисляемые имена и this

Методы класса в javascript записываются в сокращённой форме - без ключевого слова function и без : между именем и телом:

Возврат this из add включает цепочку вызовов - каждый вызов возвращает тот же экземпляр, так что следующий метод работает уже на нём.

Есть один подводный камень: методы класса не привязываются к экземпляру автоматически. Если «оторвать» метод и вызвать его отдельно, this потеряется:

Вызов g.hello() работает, потому что точка подставляет this. А вот вызов fn() сам по себе - уже нет. Если нужен «оторванный» метод с заранее привязанным контекстом (частый случай в обработчиках событий), привяжите его в конструкторе или используйте поле класса со стрелочной функцией: hello = () => \Привет, ${this.name}`;`.

Небольшой рабочий пример

Соберём всё вместе - поля класса, конструктор, методы и геттер:

Всё понятно с первого взгляда: класс чётко описывает, что такое BankAccount и что с ним можно делать.

Классы в JavaScript - это на самом деле функции

Важный момент, который стоит усвоить: class - это по большей части синтаксический сахар. Под капотом класс - это функция, а точнее функция-конструктор, а её методы живут на ClassName.prototype:

typeof User вернёт "function", а метод greet лежит на User.prototype. Экземпляры находят greet, проходя по цепочке прототипов - ровно по тому же механизму, который есть в языке с самого начала. Классы - это просто удобный синтаксис поверх него.

Такое понимание здорово выручает дальше: и наследование, и instanceof, и отладка цепочек прототипов становятся куда понятнее, если помнить - класс в JavaScript это функция, у которой на прототипе висят методы.

Что дальше: наследование

Пока что каждый класс живёт сам по себе. Но в реальных проектах классы почти всегда выстраиваются в иерархию: Dog - это разновидность Animal, AdminUser - разновидность User. За это отвечают ключевые слова extends и super - к ним и перейдём дальше.

Часто задаваемые вопросы

Как объявить класс в JavaScript?

Пишете ключевое слово class, дальше имя, а в теле - метод constructor и остальные методы. Экземпляр создаётся через new ИмяКласса(...). Например: class User { constructor(name) { this.name = name; } }, а затем new User('Ada').

Для чего нужен constructor в классе?

constructor срабатывает один раз - в момент вызова new ИмяКласса(...). Его задача - подготовить новый экземпляр: как правило, присвоить переданные аргументы в this как свойства объекта. Если вы его не опишете, JavaScript молча подставит пустой конструктор по умолчанию.

Чем класс отличается от функции в JavaScript?

Под капотом класс - это та же функция, а точнее функция-конструктор, у которой методы лежат в прототипе. class - во многом синтаксический сахар, но с важными нюансами: его нельзя вызвать без new, он даёт аккуратный extends, а методы по умолчанию становятся неперечисляемыми. В новом коде стоит использовать именно class, а не писать конструкторы руками.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ