Прототипы и классы в JavaScript: что выбрать в 2024 году
Классы в JavaScript — это синтаксический сахар над прототипами, но выбор между ними влияет на приватность полей, наследование и отладку. Разбираем, когда что писать.

В JavaScript одна и та же задача — создать несколько объектов с общим поведением — решается минимум тремя способами: фабричной функцией, прямой работой с прототипами и синтаксисом class. Все три компилируются в один и тот же механизм наследования через прототипную цепочку, но ведут себя по-разному в приватности, привязке this и наследовании. Ниже — сравнение с ценой каждого варианта и критерий, по которому выбирать. Примеры проверены на Node.js 20.x и в браузерных движках, поддерживающих ES2022 (приватные поля #).
Общий знаменатель: прототипы под капотом
Даже когда вы пишете class, движок создаёт функцию-конструктор и складывает методы в её prototype. Проверить это просто:
class Timer {
tick() { return 'tick'; }
}
// Метод лежит на прототипе, а не на самом экземпляре
const t = new Timer();
console.log(t.hasOwnProperty('tick')); // false
console.log(Timer.prototype.hasOwnProperty('tick')); // true
console.log(Object.getPrototypeOf(t) === Timer.prototype); // true
Отсюда следует главное: class не добавляет новой модели наследования. Он добавляет синтаксис, строгий режим по умолчанию, приватные поля и удобную запись extends/super. Спор «классы против прототипов» — это спор о том, где вам удобнее писать и что вы хотите скрыть, а не о двух разных движках.
Класс в JavaScript — это не сущность из мира ООП вроде Java, а обёртка над функцией-конструктором и её прототипом. Кто это помнит, тот не удивляется, почему this теряется при передаче метода как колбэка.
Три варианта на одной задаче
Возьмём счётчик с приватным значением и методами увеличения и чтения. Покажем разницу, а не обвязку.
Вариант 1. Фабричная функция с замыканием
function createCounter(start = 0) {
let value = start; // недоступно снаружи — настоящая приватность через замыкание
return {
increment() { value += 1; return value; },
get() { return value; },
};
}
const counter = createCounter(10);
counter.increment(); // 11
counter.get(); // 11
counter.value; // undefined — поле реально скрыто
Замыкание даёт настоящую приватность без синтаксиса #. Цена: каждый вызов createCounter создаёт новые копии функций increment и get. На тысяче объектов это тысяча копий каждого метода в памяти — прототип здесь не используется.
Вариант 2. Конструктор и явный прототип
function Counter(start = 0) {
this._value = start; // подчёркивание — соглашение, а не защита
}
Counter.prototype.increment = function () {
this._value += 1;
return this._value;
};
Counter.prototype.get = function () {
return this._value;
};
const c = new Counter(10);
c.increment(); // 11
c._value; // 10 — доступно снаружи, приватность мнимая
Методы лежат на прототипе — одна копия на все экземпляры, память экономится. Цена: приватности нет, поле _value видно и меняется извне. Синтаксис многословный, а если забыть new, то this в нестрогом коде укажет на глобальный объект и вы молча испортите глобальное состояние.
Вариант 3. class с приватным полем
class Counter {
#value; // приватное поле, доступно только внутри класса
constructor(start = 0) {
this.#value = start;
}
increment() {
this.#value += 1;
return this.#value;
}
get() {
return this.#value;
}
}
const c = new Counter(10);
c.increment(); // 11
// c.#value → SyntaxError на этапе парсинга: приватное поле недоступно
Здесь совмещены оба плюса: методы на прототипе (одна копия) и настоящая приватность через #. Тело класса всегда в строгом режиме, вызов без new сразу бросает TypeError, а не портит глобальную область. Приватные поля # — часть ES2022; в старых движках их нет, проверяйте цель сборки.
Сравнение вариантов
| Критерий | Фабрика + замыкание | Конструктор + prototype | class с # |
|---|---|---|---|
| Приватность полей | настоящая | нет (соглашение) | настоящая (#) |
| Методы в памяти | копия на объект | одна на прототипе | одна на прототипе |
| Наследование | вручную / композиция | вручную через prototype | extends / super |
| Забыл new | не нужен new | тихая порча this | TypeError сразу |
| this как колбэк | привязан замыканием | теряется | теряется |
| instanceof | не работает | работает | работает |
Наследование: где class экономит нервы
На прототипах цепочка наследования собирается руками, и это место, где ошибаются чаще всего:
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return this.name + ' издаёт звук'; };
function Dog(name) {
Animal.call(this, name); // вызвать родительский конструктор
}
Dog.prototype = Object.create(Animal.prototype); // связать прототипы
Dog.prototype.constructor = Dog; // восстановить ссылку — про это забывают
Dog.prototype.speak = function () { return this.name + ' лает'; };
Забыли Dog.prototype.constructor = Dog — и new Dog().constructor укажет на Animal, что ломает код, полагающийся на конструктор. То же самое через class:
class Animal {
constructor(name) { this.name = name; }
speak() { return this.name + ' издаёт звук'; }
}
class Dog extends Animal {
speak() {
return super.speak() + ', а точнее лает'; // super обращается к методу родителя
}
}
extends и super сами связывают прототипы и вызывают родительский конструктор. Ошибиться в порядке уже сложнее.
Что ломается: потеря this
И конструкторы, и классы наследуют главную боль JavaScript — this определяется в момент вызова, а не объявления. Передали метод как колбэк — контекст потерян:
const c = new Counter(0);
const fn = c.increment;
fn(); // TypeError: Cannot read private member #value from an object whose class did not declare it
// Решения:
const bound = c.increment.bind(c); // явная привязка
btn.addEventListener('click', () => c.increment()); // стрелка сохраняет внешний this
Фабрика с замыканием этой проблемы не знает: value живёт в замыкании, а не в this. Это её сильная сторона в обработчиках событий и колбэках.
Когда что выбирать
- class с # — по умолчанию для сущностей с состоянием и поведением: модели данных, сервисы, компоненты. Нужны приватность, наследование и много экземпляров.
- Фабрика с замыканием — когда объектов немного, важна строгая приватность и объект часто передаёт методы как колбэки. Также удобна, когда наследование не нужно, а нужна композиция.
- Голый prototype — почти никогда в новом коде. Оправдан в горячем пути, где нужно точно контролировать раскладку объектов, или при поддержке очень старых движков без поддержки
class.
Не смешивайте в одном модуле три стиля без причины — читающему коллеге придётся держать в голове три модели создания объектов.
Что делать дальше
Если пишете под современные движки — берите class с приватными полями и проверьте, что цель сборки (TypeScript target, Babel-пресеты) не транспилирует # в WeakMap без надобности: это меняет производительность и отладку. Замерьте раскладку объектов в реальном профиле памяти, если экземпляров десятки тысяч. Наследование глубже двух уровней — сигнал подумать о композиции вместо extends: это отдельная тема, здесь разбирали только выбор между прототипами и классами.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: MDN — Classes (JavaScript), MDN — Inheritance and the prototype chain
Частые вопросы
Класс в JavaScript быстрее, чем конструктор с прототипом?
А если у меня старый движок без поддержки приватных полей #?
Почему у меня падает ошибка при передаче метода класса в колбэк?
Работает ли instanceof с фабричными функциями?
Можно ли добавить метод в class после объявления, как в prototype?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.