Прототипы и классы
JavaScript — язык с прототипным наследованием, а не классовым, как Java или C++. Даже синтаксис class из ES6 — это «синтаксический сахар» над прототипами. На собеседованиях часто спрашивают именно о том, что скрывается под этим сахаром: цепочки прототипов, конструкторы, Object.create, статические и приватные члены.
Содержание
- Что такое прототип
- Цепочка прототипов
- Object.create, setPrototypeOf, getPrototypeOf
- Конструкторы и
prototype - Наследование без
class - Синтаксис
class extendsиsuper- Приватные поля и методы
# - Статические методы и свойства
instanceofи его ограничения- Чем
classв JS отличается от классов в Java/C++ - Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Что такое прототип
Каждый объект в JavaScript имеет скрытое внутреннее свойство [[Prototype]]. Оно ссылается на другой объект — прототип. Когда движок ищет свойство или метод, он сначала смотрит в самом объекте, затем в его прототипе, затем в прототипе прототипа — и так до null.
const animal = {
eats: true,
walk() {
console.log("I'm walking");
}
};
const dog = {
barks: true
};
Object.setPrototypeOf(dog, animal);
console.log(dog.eats); // true — найдено в прототипе
console.log(dog.barks); // true — найдено в самом объекте
dog.walk(); // "I'm walking" — найдено в прототипе
Если свойство не найдено ни в одном звене цепочки, возвращается undefined.
Цепочка прототипов
Цепочка прототипов — это последовательность объектов, по которой движок ищет свойства. Она заканчивается null:
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
__proto__ — устаревший, но широко известный геттер/сеттер для [[Prototype]]. В современном коде лучше использовать Object.getPrototypeOf / Object.setPrototypeOf.
Важно: прототипное наследование — это делегирование, а не копирование. dog не содержит метод walk, он лишь делегирует его поиск прототипу animal.
Object.create, setPrototypeOf, getPrototypeOf
Object.create(proto) создаёт новый объект с указанным прототипом:
const animal = { eats: true };
const rabbit = Object.create(animal);
console.log(rabbit.eats); // true
console.log(Object.getPrototypeOf(rabbit) === animal); // true
Можно передать второй аргумент — описатели свойств:
const user = Object.create(null, {
name: { value: 'Alice', writable: true, enumerable: true }
});
Object.setPrototypeOf меняет прототип уже существующего объекта. Это медленная операция, поэтому менять прототип «на лету» не рекомендуется — лучше создать объект с нужным прототипом сразу.
Object.getPrototypeOf возвращает прототип объекта — безопасная альтернатива __proto__.
Конструкторы и prototype
До появления class наследование делали через функции-конструкторы и свойство prototype:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, ${this.name}`);
};
const alice = new Person('Alice');
alice.greet(); // "Hello, Alice"
Что происходит при new Person('Alice'):
- Создаётся новый пустой объект.
- Его
[[Prototype]]устанавливается наPerson.prototype. - Вызывается
Personсthis, указывающим на новый объект. - Возвращается этот объект.
Методы хранятся в Person.prototype, поэтому все экземпляры разделяют одну и ту же функцию — она не дублируется в памяти.
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
Наследование без class
Чтобы реализовать наследование вручную, нужно связать прототипы и вызвать родительский конструктор:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name} makes a sound`);
};
function Dog(name, breed) {
Animal.call(this, name); // вызываем родительский конструктор
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // восстанавливаем constructor
Dog.prototype.bark = function () {
console.log(`${this.name} barks`);
};
const rex = new Dog('Rex', ' shepherd');
rex.speak(); // "Rex makes a sound"
rex.bark(); // "Rex barks"
Почему именно Object.create(Animal.prototype), а не new Animal()? Потому что new Animal() мог бы выполнить лишний код конструктора и создать ненужные побочные эффекты. Object.create лишь устанавливает прототип.
Синтаксис class
class делает то же самое, но читабельнее:
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}`);
}
}
const alice = new Person('Alice');
alice.greet(); // "Hello, Alice"
console.log(typeof Person); // "function"
console.log(alice.__proto__ === Person.prototype); // true
Методы внутри class автоматически попадают в Person.prototype. Тело класса выполняется в строгом режиме (use strict).
Важное отличие: функции-конструкторы поднимаются полностью, а классы — нет. Класс остаётся в TDZ до строки объявления:
const dog = new Dog(); // ReferenceError
class Dog {}
extends и super
extends связывает прототипы, а super вызывает методы родителя:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // обязательно до использования this
this.breed = breed;
}
speak() {
super.speak(); // вызов метода родителя
console.log(`${this.name} barks`);
}
}
const rex = new Dog('Rex', 'Shepherd');
rex.speak();
// "Rex makes a sound"
// "Rex barks"
В производном классе конструктор обязан вызвать super() до первого обращения к this. В непроизводном классе this создаётся автоматически.
Приватные поля и методы #
Современный синтаксис позволяет объявлять настоящие приватные члены класса, недоступные снаружи:
class BankAccount {
#balance = 0;
deposit(amount) {
if (amount <= 0) throw new Error('Invalid amount');
this.#balance += amount;
}
withdraw(amount) {
if (amount > this.#balance) throw new Error('Insufficient funds');
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(1000);
console.log(account.getBalance()); // 1000
console.log(account.#balance); // SyntaxError: Private field must be declared
Приватные поля:
- доступны только внутри класса, где объявлены;
- не наследуются напрямую (потомок не может обратиться к
#balanceродителя); - нельзя получить через
Reflect.ownKeysили подобные методы; - настоящие приватные члены, а не просто соглашение об именовании.
Можно объявлять и приватные методы:
class User {
#validateName(name) {
return name.trim().length > 0;
}
constructor(name) {
if (!this.#validateName(name)) throw new Error('Invalid name');
this.name = name;
}
}
Статические методы и свойства
Статические члены принадлежат самому классу, а не его экземплярам. Они часто используются для фабрик, утилит и констант:
class User {
static role = 'guest';
static createAdmin(name) {
const user = new User(name);
user.role = 'admin';
return user;
}
constructor(name) {
this.name = name;
}
}
console.log(User.role); // "guest"
const admin = User.createAdmin('Alice');
console.log(admin.role); // "admin"
Статические методы наследуются. Внутри статического метода this ссылается на сам класс:
class Animal {
static isAnimal(obj) {
return obj instanceof this;
}
}
class Dog extends Animal {}
console.log(Dog.isAnimal(new Dog())); // true
instanceof и его ограничения
instanceof проверяет, есть ли класс/функция-конструктор в цепочке прототипов объекта:
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true
Под капотом instanceof использует prototype: rex.__proto__.__proto__... === Dog.prototype.
Ограничения
-
Не работает с примитивами.
'hello' instanceof String; // falseДля примитивов используйте
typeofилиObject.prototype.toString.call. -
Зависит от прототипа, который можно изменить.
const arr = []; Object.setPrototypeOf(arr, null); console.log(arr instanceof Array); // false -
Не учитывает приватные поля.
instanceofговорит лишь о цепочке прототипов, а не о том, есть ли у объекта нужные внутренние поля. -
С объектами из других iframe/Window прототипы различаются.
Arrayиз одного окна не равенArrayиз другого, поэтомуinstanceofможет датьfalseдля валидного массива.
В таких случаях надёжнее использовать Array.isArray(obj) или Object.prototype.toString.call(obj).
Чем class в JS отличается от классов в Java/C++
| Аспект | JavaScript | Java/C++ |
|---|---|---|
| Модель наследования | Прототипная | Классовая |
| Класс — это | функция-конструктор + объект prototype | чертёг/тип во время компиляции |
| Методы | хранятся в prototype, общие для всех экземпляров | обычно часть каждого объекта или vtable |
| Множественное наследование | Нет, только цепочка прототипов | Есть (Java — интерфейсы, C++ — множественное) |
| Приватность | #поля (настоящая) или замыкания | модификаторы доступа |
| Расширение объектов | можно менять прототипы на лету | нельзя |
В JavaScript «класс» — это договорённость и удобный синтаксис. Под капотом всё равно работают прототипы.
Ключевые тезисы для интервью
- Каждый объект имеет скрытое свойство
[[Prototype]], ссылающееся на другой объект; поиск свойства идёт по цепочке прототипов доnull. Object.create(proto)создаёт новый объект с заданным прототипом, аObject.setPrototypeOfменяет прототип существующего объекта — это медленная операция.- Наследование без
classреализуется через функции-конструкторы, связывание прототипов черезObject.createи вызов родительского конструктора через.call. - При
new Constructor()создаётся новый объект, его[[Prototype]]связывается сConstructor.prototype, вызывается конструктор, и объект возвращается. super()в производных классах вызывает конструктор родителя и обязателен до первого обращения кthis.- Приватные поля и методы объявляются через
#и доступны только внутри класса, где объявлены; они не наследуются. - Статические методы принадлежат самому классу, а не экземплярам; они используются для фабрик и утилит и наследуются подклассами.
instanceofпроверяет наличие конструктора в цепочке прототипов, но может обмануть при изменённом прототипе или объектах из других iframe.- Классы в JavaScript — это синтаксический сахар над прототипным наследованием, в отличие от классового наследования в Java/C++.
Заключение
JavaScript использует прототипное наследование: каждый объект делегирует поиск свойств своему прототипу. Синтаксис class из ES6 упрощает работу, но под капотом остаются те же прототипы, конструкторы и цепочки. Понимание этих механизмов необходимо для написания корректного кода, прохождения собеседований и эффективного использования наследования.