Что такое классы в JavaScript и как они соотносятся с прототипами?
Вопрос
Что представляют собой ES6-классы под капотом, как работают extends/super, приватные поля (#field) и статические члены?
Короткий ответ
class в JavaScript — это синтаксический сахар над прототипным наследованием: под капотом методы класса по-прежнему кладутся в prototype, а не в сам объект. extends настраивает цепочку прототипов между классами, super даёт доступ к родительскому конструктору и методам. Приватные поля (#field) обеспечивают настоящую инкапсуляцию на уровне движка, а static члены принадлежат самому классу, а не экземплярам.
Подробный ответ
Как устроено прототипное наследование в деталях — отдельная большая тема; здесь фокус на том, что даёт именно синтаксис class.
class как сахар. Объявление class Foo {} создаёт функцию-конструктор, а все методы, объявленные в теле класса, добавляются в Foo.prototype — точно так же, как при ручной настройке через function + Foo.prototype.method = .... Отличия от "ручного" варианта:
- Методы класса не перечисляемы (
enumerable: false), в отличие отFoo.prototype.method = fn. - Класс нельзя вызвать без
new— попытка бросаетTypeError. - Тело класса всегда выполняется в строгом режиме (
'use strict'). - Классы не поднимаются как объявления функций — они находятся в temporal dead zone до объявления.
extends и super. class Dog extends Animal делает две вещи: Dog.prototype.__proto__ = Animal.prototype (наследование методов экземпляра) и Dog.__proto__ = Animal (наследование статических методов). Вызов super(...) в конструкторе вызывает конструктор родителя и обязателен до обращения к this, если класс что-то наследует — движок не создаёт this автоматически при наличии родителя, это делает super(). super.method() в обычном методе вызывает метод родительского прототипа, минуя переопределение в дочернем классе.
Приватные поля (#field). Начиная с ES2022, поля с префиксом # доступны только изнутри тела класса — это соблюдается на уровне синтаксиса и рантайма, а не соглашение об именовании (как раньше делали через _field). Обращение instance.#field снаружи класса — SyntaxError на этапе парсинга. Есть также приватные методы (#method()) и приватные статические поля.
static члены. Свойства и методы, объявленные с static, живут на самом классе (конструкторе), а не на prototype, то есть недоступны через экземпляр. Используются для фабричных методов, счётчиков экземпляров, констант, утилит, логически привязанных к классу, но не требующих состояния конкретного объекта.
Пример
class Animal {
static kingdom = 'Animalia' // статическое поле
#energy = 100 // приватное поле
constructor(name) {
this.name = name
}
static create(name) {
// статический фабричный метод
return new Animal(name)
}
eat(amount) {
this.#energy += amount
return this.#energy
}
speak() {
return `${this.name} издаёт звук`
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name) // обязателен до обращения к this
this.breed = breed
}
speak() {
const base = super.speak() // вызов метода родителя
return `${base} — гав! (${this.breed})`
}
}
const rex = new Dog('Рекс', 'Лабрадор')
console.log(rex.speak()) // "Рекс издаёт звук — гав! (Лабрадор)"
console.log(rex.eat(10)) // 110
console.log(Dog.kingdom) // "Animalia" — статика наследуется тоже
// console.log(rex.#energy) // SyntaxError: Private field must be declared
// Под капотом это всё ещё прототипы
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype) // true
console.log(typeof Dog) // "function" — класс это функция
console.log(Dog.speak === undefined) // true — методы экземпляра не на классе
Дополнительные вопросы
- Почему нельзя обратиться к
thisв конструкторе наследника до вызоваsuper()? - Чем приватные поля (
#field) отличаются от замыканий как способа скрыть состояние? - Как реализовать геттеры/сеттеры в классе и когда это уместнее обычных методов?