Прототипы и классы в JavaScript: чек-лист перед продакшеном
Класс в JS — синтаксический сахар над прототипами, и это протекает: наследование ломается на стрелках, приватность оказывается не приватной, а сравнение через instanceof подводит на границах. Чек-лист перед релизом.

Строчка class User {} выглядит как класс из Java, но под ней всё те же прототипы, что и в ES5. Пока вы не наступаете на границы, разница незаметна. Наступаете — на трёх местах: методы, потерявшие this при передаче в колбэк; поля, которые считали приватными, а они читаются из консоли; и instanceof, который врёт, если объект пришёл из другого realm или прототип подменили. Ниже — что проверить в коде перед тем, как он поедет в прод. Актуально для сред с поддержкой ES2022 и новее: Node.js 16+, современные браузеры.
Что именно скрывает синтаксис класса
Объявление класса создаёт функцию-конструктор, а методы кладёт в её prototype. Разница с ES5 не в модели, а в деталях, которые легко упустить:
- тело класса выполняется в строгом режиме всегда, даже без
"use strict"; - методы прототипа не перечисляются в
for...in— они non-enumerable, в отличие от ручного присваиванияFoo.prototype.method = ...; - объявление класса не поднимается (hoisting) как функция — до строки объявления обращение к классу даёт
ReferenceError; - вызов конструктора без
newбросаетTypeError, тогда как обычную функцию-конструктор можно случайно позвать безnewи получить мусор.
Проверьте, что код не полагается на перечислимость методов и не вызывает конструкторы функциями. Это первый пункт ревью.
this теряется — и это не баг класса
Самая частая продовая поломка. Метод класса — обычная функция, у которой this определяется в момент вызова, а не в момент объявления. Передали метод как колбэк — контекст потерян.
// Node.js 16+, браузеры с ES2022
class Counter {
count = 0;
// обычный метод: this зависит от того, КАК его вызвали
increment() {
this.count += 1;
}
// поле со стрелкой: this привязан к экземпляру навсегда
incrementBound = () => {
this.count += 1;
};
}
const counter = new Counter();
const detached = counter.increment;
// detached(); // TypeError: Cannot read properties of undefined
setTimeout(counter.increment, 0); // this === undefined в strict
setTimeout(counter.incrementBound, 0); // работает: this закреплён
Стрелочное поле решает проблему, но у него есть цена: функция создаётся заново на каждый экземпляр и лежит не в прототипе, а в самом объекте. Для класса с тысячами экземпляров и десятком таких методов это лишняя память и лишние аллокации.
Стрелочное поле — это не «метод, который правильно работает». Это отдельная функция на каждый объект. Один обработчик события — норма. Десять методов бизнес-логики через стрелки — утечка внимания, а иногда и памяти.
Что проверить: любой метод, который вы передаёте куда-то по ссылке (обработчик события, setTimeout, .map(this.handler)), либо привязывайте явно через .bind(this), либо делайте стрелочным полем, либо оборачивайте: () => counter.increment(). Без этого — тихая ошибка в проде.
Три способа зафиксировать this — что выбрать
| Способ | Где живёт функция | Цена | Когда |
|---|---|---|---|
Стрелочное поле m = () => {} | в экземпляре | память на каждый объект | 1–2 обработчика на класс |
this.m = this.m.bind(this) в конструкторе | в экземпляре (обёртка) | та же память + шум в конструкторе | нужен и метод в прототипе, и привязанная версия |
Обёртка в месте вызова () => obj.m() | в прототипе, обёртка временная | ноль лишней памяти на экземпляр | много экземпляров, редкая передача по ссылке |
Приватность: # против соглашения об именах
Поле с _ в начале — не приватное, это просьба. Оно читается и меняется снаружи. Настоящая приватность появилась с приватными полями через # (ES2022, поддержка в Node.js 12+ частично, стабильно с 14+):
class Account {
#balance = 0; // недоступно снаружи вообще
_legacyBalance = 0; // "приватность" по соглашению
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const acc = new Account();
acc._legacyBalance = 999; // молча сработает
// acc.#balance; // SyntaxError ещё на этапе парсинга
Важная разница: #balance — не свойство объекта в обычном смысле. Оно не видно в Object.keys, не сериализуется JSON.stringify и недоступно через Reflect. Если у вас есть код, который сериализует объекты или обходит их поля рефлексией, приватные поля из него выпадут — это может быть и плюсом, и источником пропавших данных в логах.
Что проверить перед продом: если данные должны попадать в JSON — не держите их в #-полях без явного метода сериализации. Если проверяете «есть ли у объекта приватное поле», используйте #field in obj — это безопасно и не бросает исключение для чужих объектов.
instanceof и границы realm
instanceof проверяет, есть ли Class.prototype в цепочке прототипов объекта. Это ломается в двух случаях:
- Разные realm. Объект пришёл из iframe, из воркера или из другого контекста Node.js (например, из
vm-модуля). ЕгоArray— не вашArray, иarr instanceof Arrayвернётfalse, хотя это массив. Для встроенных типов используйтеArray.isArray(), а для остальных — проверку по утиному признаку илиSymbol.hasInstance. - Подмена прототипа. Кто-то вызвал
Object.setPrototypeOf— и цепочка изменилась. Редко, но в коде с миксинами встречается.
На границе с внешними данными (десериализация, ответ API, объект из библиотеки) instanceof — ненадёжный контракт. Проверяйте форму объекта, а не его происхождение.
super и порядок инициализации полей
Порядок в конструкторе наследника строгий, и его нарушают:
- До вызова
super()нельзя обращаться кthis— будетReferenceError. - Поля класса (
field = value) инициализируются сразу послеsuper(), а не в начале конструктора. Значит, родительский конструктор выполняется до того, как поля наследника получат значения.
class Base {
constructor() {
// на этот момент поле name у наследника ещё undefined
this.describe();
}
describe() {}
}
class Named extends Base {
name = 'default'; // присвоится ПОСЛЕ super()
describe() {
console.log(this.name); // выведет undefined при вызове из Base
}
}
new Named(); // undefined, а не 'default'
Если родитель вызывает переопределённый метод из конструктора — классическая ловушка. Наследник ещё не проинициализирован. Что проверить: конструкторы базовых классов не должны звать методы, которые наследник переопределяет с опорой на свои поля.
Чек-лист перед мержем
- Ни один метод не передаётся по ссылке без
bind, стрелки или обёртки. - Стрелочных полей — минимум; в классах с массовыми экземплярами методы лежат в прототипе.
- Секретные данные — в
#-полях, а не под_; при этом сериализация таких объектов проверена отдельно. - Проверка типа на границе с внешними данными — по форме объекта или
Array.isArray, а не голымinstanceof. - Конструкторы базовых классов не вызывают переопределяемые методы, зависящие от полей наследника.
- Классы не вызываются без
new(линтер это ловит).
Что настроить дальше
Включите в ESLint правила class-methods-use-this, no-invalid-this и @typescript-eslint/unbound-method (для TS-проектов) — последнее ловит именно потерю this при передаче метода по ссылке. Если пишете на TypeScript, модификатор private проверяется только компилятором и стирается в рантайме — для настоящей приватности комбинируйте его с #-полями. Померьте память на классе с большим числом экземпляров до и после перевода стрелочных полей в прототип: разница видна в heap snapshot Chrome DevTools или через process.memoryUsage() в Node.js на нагрузочном прогоне.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: MDN: Classes — JavaScript, MDN: Private class features
Частые вопросы
У меня старая среда без поддержки #-полей. Что делать?
Стрелочные методы всегда лучше обычных, раз с this проблем нет?
instanceof вернул false для явно правильного объекта — что не так?
Почему поле наследника оказалось undefined в методе, вызванном из конструктора родителя?
Приватное #-поле пропало из JSON и логов — это нормально?
TypeScript private и JavaScript # — это одно и то же?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.