Прототипы и классы в JavaScript: типичные ошибки
Стрелочные функции ломают методы, забытый new портит this, а общий массив в прототипе течёт между всеми экземплярами. Разбираем частые ошибки с прототипами и классами и показываем, как их не допустить.

Вы пишете класс, объявляете метод стрелочной функцией, а потом не можете понять, почему this ведёт себя не так. Или наследуете один класс от другого, забываете super() — и получаете ReferenceError ещё до первой строки конструктора. Прототипы в JavaScript работают не так, как классы в Java или Python, а синтаксис class из ES2015 маскирует эту разницу, а не убирает её. Ниже — приёмы против ошибок, которые чаще всего всплывают на код-ревью и в проде. Актуально для современного JavaScript (ES2015 и новее, включая приватные поля из ES2022), исполняется в Node.js 18+ и любом современном браузере.
Класс — это по-прежнему прототип
Ключевое, из-за чего возникает половина ошибок: class — синтаксический сахар над функциями-конструкторами и прототипным наследованием. Методы, объявленные в теле класса, не копируются в каждый экземпляр — они лежат на ClassName.prototype и находятся через цепочку прототипов. Понимание этого напрямую влияет на то, где хранить состояние, а где — поведение.
Экземпляров может быть тысяча, но метод в памяти один — он живёт на прототипе. Поэтому данные храните в экземпляре, а функции — в прототипе. Нарушите это правило наоборот — получите либо утечку общего состояния, либо лишние аллокации.
Ошибка 1. Стрелочная функция вместо метода
Стрелочные функции не имеют собственного this — они берут его из лексического окружения на момент объявления. Метод класса, записанный стрелкой как поле, ведёт себя иначе, чем обычный метод, и это ломает наследование.
class Timer {
// обычный метод — лежит на Timer.prototype
start() {
return this.seconds;
}
// поле-стрелка — своё для каждого экземпляра, this привязан жёстко
stop = () => {
return this.seconds;
};
}
Разница практическая. Метод start можно переопределить в наследнике и вызвать через super.start(). Поле-стрелку stop из наследника через super вызвать нельзя — стрелки нет в цепочке прототипов. Плюс каждое поле-стрелка создаёт новую функцию на каждый экземпляр: тысяча объектов — тысяча функций.
Когда стрелка-поле оправдана: коллбэк, который передаётся наружу и должен сохранить this, например обработчик события. Тогда фиксированный this — это то, что вам нужно, и цена в виде функции на экземпляр осознанная.
Ошибка 2. Забытый new
Функцию-конструктор без new вызвать можно — синтаксис не запретит. Но this внутри окажется не новым объектом, а глобальным (в нестрогом режиме) или undefined (в строгом и в модулях). Присваивания полям либо загрязнят глобальный объект, либо упадут с TypeError.
function Point(x, y) {
this.x = x;
this.y = y;
}
const p = Point(1, 2); // без new: this === undefined в модуле
// TypeError: Cannot set properties of undefined (setting 'x')
Классы, объявленные через class, защищены: вызов класса без new всегда бросает TypeError: Class constructor cannot be invoked without 'new'. Это одна из причин предпочитать class функциям-конструкторам в новом коде.
Ошибка 3. Мутируемое значение в прототипе
Если положить массив или объект прямо на прототип, он станет общим для всех экземпляров. Каждый push в одном объекте увидят все остальные.
function Cart() {}
Cart.prototype.items = []; // общий массив на всех
const a = new Cart();
const b = new Cart();
a.items.push('книга');
console.log(b.items); // ['книга'] — b не трогали, а он изменился
Присваивание a.items.push(...) не создаёт своё поле у a — оно мутирует общий массив по ссылке. Инициализируйте изменяемые данные в конструкторе или в поле экземпляра:
class Cart {
items = []; // своё поле для каждого экземпляра
}
Ошибка 4. super и наследование
В производном классе, если вы объявили свой конструктор, вызвать super() надо до первого обращения к this. Пропустите — ReferenceError: Must call super constructor ... before accessing 'this'. Порядок жёсткий: сначала родитель конструирует объект, потом наследник его дополняет.
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // сначала это
this.breed = breed; // потом обращение к this
}
}
Ещё частая ошибка — переопределить метод, но забыть про поведение родителя. Если наследник должен расширить, а не заменить логику, вызывайте super.methodName() внутри. Прямой обход через ParentClass.prototype.method.call(this) работает, но хрупок при дальнейшем наследовании — используйте super.
Ошибка 5. Приватность через соглашение, а не язык
Долгое время «приватные» поля обозначали подчёркиванием: this._token. Это документация, а не защита — снаружи такое поле читается и пишется как любое другое. С ES2022 в языке есть настоящие приватные поля через префикс #.
class Account {
#balance = 0; // приватное, недоступно снаружи
deposit(sum) {
this.#balance += sum;
return this.#balance;
}
}
const acc = new Account();
acc.deposit(100);
// acc.#balance — SyntaxError на этапе разбора
Обращение к #balance вне тела класса — синтаксическая ошибка, а не undefined. Оговорка: приватные поля не сериализуются JSON.stringify и не видны в Object.keys — если вам нужно сохранять состояние, продумайте это заранее.
Сравнение подходов к состоянию и поведению
| Что кладём | Куда | Результат |
|---|---|---|
| Метод (функция) | тело класса (прототип) | один экземпляр в памяти, работает super |
| Изменяемые данные (массив, объект) | поле экземпляра / конструктор | своё для каждого объекта |
| Изменяемые данные | прототип | общее состояние — почти всегда баг |
| Коллбэк с фиксированным this | поле-стрелка | this сохранён, но функция на каждый экземпляр |
| Приватное состояние | поле с # | недоступно снаружи, не сериализуется |
Когда что выбирать
- Обычный метод — по умолчанию для любого поведения класса. Экономит память, участвует в наследовании.
- Поле-стрелка — только когда метод уходит наружу как коллбэк и должен помнить свой
this. В остальных случаях привязку решает.bind(this)в точке передачи или обычный метод. - Поле экземпляра — для любого изменяемого состояния, всегда.
- Приватное поле
#— когда нужна реальная инкапсуляция, а не намёк подчёркиванием.
Что проверить на ревью
Три формулировки замечаний, которые ловят большинство описанных ошибок:
- «Здесь массив на прототипе — состояние утечёт между экземплярами, перенеси в поле или конструктор.»
- «Метод объявлен стрелкой без причины — теряем прототип и
super, а получаем функцию на каждый объект. Он куда-то передаётся коллбэком?» - «В конструкторе наследника обращение к
thisдоsuper()— упадёт при первом создании.»
Что делать дальше
Включите в проект eslint с правилами, которые ловят часть этих ошибок статически: no-invalid-this, class-methods-use-this, no-dupe-class-members. Точные имена и доступность правил сверяйте в документации вашей версии ESLint — набор правил меняется между мажорными релизами. Если проект на TypeScript, часть проблем (забытый new, обращение к приватному полю снаружи) отсекается ещё на этапе компиляции. Следующая тема для разбора — как ведёт себя this при потере контекста в коллбэках и что именно делает bind; здесь мы её только обозначили.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Classes — JavaScript | MDN, Inheritance and the prototype chain — MDN
Частые вопросы
А если у меня старый код на функциях-конструкторах, стоит переписывать на class?
Приватные поля с # точно работают в моём окружении?
Почему мой метод-стрелка не переопределяется в наследнике?
Как понять, что состояние утекает через прототип?
instanceof перестал работать после того, как я вернул объект из конструктора — почему?
Не сработало: получаю TypeError про super constructor — что не так?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.