Прототипы — как ООП работает под капотом
Мы научились писать классы и наследование — это современный, удобный синтаксис. Но за ним скрывается нечто интересное: уникальный механизм JavaScript, который называется прототипная цепочка. Понять его — значит понять JavaScript по-настоящему.
🔍 У каждого объекта есть скрытый «родитель»
В JavaScript у каждого объекта есть скрытое внутреннее свойство [[Prototype]]. Квадратные скобки означают, что это не обычное свойство — оно невидимо снаружи, но активно используется движком JavaScript.
[[Prototype]] — это ссылка на другой объект. Когда JavaScript ищет свойство или метод в объекте и не находит его — он автоматически идёт по этой ссылке в объект-прототип и ищет там. Если и там не нашёл — идёт дальше по цепочке. Это и есть прототипная цепочка.
🔗 Прототипная цепочка (prototype chain) — это последовательность объектов, связанных через [[Prototype]]. Поиск свойств идёт вверх по цепочке, пока не найдёт нужное или не дойдёт до null.
Давайте сделаем это видимым — через Object.getPrototypeOf():
const arr = [1, 2, 3];
// У массива есть прототип — Array.prototype
const arrProto = Object.getPrototypeOf(arr);
console.log(arrProto === Array.prototype); // true
// У Array.prototype есть прототип — Object.prototype
const arrProtoProto = Object.getPrototypeOf(arrProto);
console.log(arrProtoProto === Object.prototype); // true
// У Object.prototype прототип — null (конец цепочки)
const end = Object.getPrototypeOf(arrProtoProto);
console.log(end); // null
Цепочка для обычного массива: [1,2,3] → Array.prototype → Object.prototype → null. Именно поэтому на массиве работают и методы массивов (push, map), и методы объектов (toString, hasOwnProperty).
🔧 __proto__ и Object.getPrototypeOf()
Исторически доступ к [[Prototype]] предоставляло свойство __proto__ (двойное нижнее подчёркивание с обеих сторон, произносится «дандер-прото»). Оно работало во всех браузерах, но технически считалось «нестандартным».
const dog = { name: "Рекс" };
const animal = { breathe() { return "дышу"; } };
// Устанавливаем прототип через __proto__ (устаревший способ)
dog.__proto__ = animal;
// Теперь dog "видит" метод из animal
console.log(dog.breathe()); // дышу
// Сам метод не принадлежит dog
console.log(dog.hasOwnProperty("breathe")); // false
console.log(dog.hasOwnProperty("name")); // true
В современном коде лучше использовать стандартные методы:
obj.__proto__
obj.__proto__ = proto
Object.getPrototypeOf(obj)
Object.setPrototypeOf(obj, proto)
⚙️ Как class работает под капотом
Когда JavaScript видит:
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log("Привет, " + this.name);
}
}
Под капотом это превращается примерно в следующее:
// Что реально происходит "под капотом":
function User(name) {
this.name = name; // конструктор — просто функция
}
User.prototype.greet = function() { // метод — на прототипе!
console.log("Привет, " + this.name);
};
// new User("Анна") делает примерно следующее:
// 1. Создаёт пустой объект {}
// 2. Устанавливает его [[Prototype]] = User.prototype
// 3. Вызывает функцию User с this = новый объект
// 4. Возвращает этот объект
Теперь всё складывается! Когда вы вызываете anna.greet():
- JavaScript ищет
greetв самом объектеanna— не находит - Идёт по
[[Prototype]]вUser.prototype— находитgreet! - Вызывает
greetсthis = anna
Вот почему методы хранятся в прототипе, а не в каждом объекте: если у вас 1000 пользователей, то метод greet существует в одном месте — User.prototype. Все 1000 объектов ссылаются на него через цепочку прототипов. Это экономит память. Если бы метод копировался в каждый объект — у вас было бы 1000 копий одной и той же функции.
🗺️ Визуализация: цепочка прототипов
Давайте нарисуем, как выглядит цепочка для наследования Dog extends Animal:
{ name: "Рекс", breed: "Лабрадор" }
[[Prototype]] → Dog.prototype
{ bark(), fetch() }
[[Prototype]] → Animal.prototype
{ eat(), sleep(), describe() }
[[Prototype]] → Object.prototype
{ toString(), hasOwnProperty(), ... }
[[Prototype]] → null (конец цепочки)
Когда вы вызываете rex.eat() — JavaScript проходит вверх по цепочке: нет в rex → нет в Dog.prototype → есть в Animal.prototype! Вот так работает наследование «под капотом».
class Animal {
constructor(name) { this.name = name; }
eat() { console.log(this.name + " ест"); }
}
class Dog extends Animal {
bark() { console.log("Гав!"); }
}
const rex = new Dog("Рекс");
// Проверим цепочку:
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
// rex.eat() работает — метод найден в Animal.prototype
rex.eat(); // Рекс ест
// hasOwnProperty — из Object.prototype (конец цепочки!)
console.log(rex.hasOwnProperty("name")); // true
console.log(rex.hasOwnProperty("eat")); // false — метод не в объекте!
🛠️ Object.create() — создать объект с прототипом
Object.create(proto) создаёт новый объект и устанавливает ему прототип. Это низкоуровневый способ работы с прототипами — без классов:
const animalMethods = {
eat() { console.log(this.name + " ест."); },
sleep() { console.log(this.name + " спит."); }
};
// Создаём объект с прототипом animalMethods
const dog = Object.create(animalMethods);
dog.name = "Рекс";
dog.bark = function() { console.log("Гав!"); };
dog.eat(); // Рекс ест. ← из прототипа
dog.bark(); // Гав! ← своё
// Это то, что делает class под капотом
console.log(Object.getPrototypeOf(dog) === animalMethods); // true
💡 Object.create(null) создаёт объект без прототипа — без toString, hasOwnProperty и других встроенных методов. Иногда полезно для чистых словарей-хэшмап.
🌍 В JavaScript всё — объект (почти)
Теперь вы можете понять знаменитую фразу «в JavaScript всё является объектом»:
// Строка — примитив, но при обращении к методам оборачивается в объект
"hello".toUpperCase(); // "HELLO"
// Это работает потому что String.prototype.toUpperCase существует!
// Массив — объект с прототипом Array.prototype
[].push // есть, в Array.prototype
[].map // есть, в Array.prototype
// Функция — тоже объект с прототипом Function.prototype
function greet() {}
greet.call // есть, в Function.prototype
greet.bind // есть, в Function.prototype
// Все цепочки в итоге доходят до Object.prototype
// а затем до null
Вот почему у строки есть .length, .toUpperCase(), .split() — всё это методы из String.prototype. При обращении к методу примитива JavaScript временно «оборачивает» его в объект-обёртку, находит метод в прототипе и вызывает его.
🎯 Практическое применение этих знаний
В современном коде вы редко будете работать с прототипами напрямую — для этого есть классы. Но понимание прототипов поможет вам:
Понять, почему объект «видит» методы, которых у него нет. Как работает поиск по цепочке.
Старый JS-код (до 2015) написан на прототипах без class. Понимание позволит его читать и поддерживать.
«Как работают прототипы?», «Чем class отличается от функции-конструктора?» — классические вопросы на JS-собеседованиях.
Знание о том, что методы в прототипе — одна копия на все объекты. Не создавай методы внутри конструктора — они дублируются!
⚠️ Не делай так: не добавляй свои методы в прототипы встроенных объектов (Array.prototype.myMethod, String.prototype.trim2). Это называется «monkey patching» и может сломать чужой код, который тоже работает с этими объектами.
📊 class vs прототипы: что выбирать
Для нового кода всегда используйте class. Это современный стандарт, читабельно, поддерживает приватные поля, статические методы и геттеры/сеттеры из коробки. Прототипы — это то, что работает под капотом, но не то, что вы должны трогать руками в новых проектах.
Знание прототипов — это ваше «понимание движка». Как механик знает, что под капотом автомобиля. Ездить на машине можно и не зная, как работает двигатель — но настоящий мастер знает обе стороны.
✅ Итоги урока
- Каждый объект имеет скрытое [[Prototype]] — ссылку на другой объект (прототип)
- Цепочка прототипов: при поиске свойства JavaScript идёт вверх по [[Prototype]] до null
Object.getPrototypeOf(obj)— правильный способ получить прототип объектаclassпод капотом — это функция-конструктор + методы вClassName.prototype- Методы хранятся в прототипе (не в каждом объекте) — экономия памяти
Object.create(proto)— создать объект с явно заданным прототипом- В современном коде используй
class— прототипы нужны для понимания, не для прямого использования - Не добавляй свои методы в прототипы встроенных объектов (
Array.prototype)
Поздравляем — раздел «Объектно-ориентированный JavaScript» завершён! Вы освоили методы и this, геттеры/сеттеры, классы, наследование и прототипы — фундамент JavaScript-ООП. 🎉
Прототипы в JavaScript
7 вопросов