$ sudo teach IT
РАЗДЕЛ 8 · УРОК 5

Прототипы — как ООП работает под капотом

Мы научились писать классы и наследование — это современный, удобный синтаксис. Но за ним скрывается нечто интересное: уникальный механизм JavaScript, который называется прототипная цепочка. Понять его — значит понять JavaScript по-настоящему.

⏱ ~25 минут 🎓 Концептуальный урок ⚡ 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():

  1. JavaScript ищет greet в самом объекте anna — не находит
  2. Идёт по [[Prototype]] в User.prototype — находит greet!
  3. Вызывает greet с this = anna

Вот почему методы хранятся в прототипе, а не в каждом объекте: если у вас 1000 пользователей, то метод greet существует в одном месте — User.prototype. Все 1000 объектов ссылаются на него через цепочку прототипов. Это экономит память. Если бы метод копировался в каждый объект — у вас было бы 1000 копий одной и той же функции.

🗺️ Визуализация: цепочка прототипов

Давайте нарисуем, как выглядит цепочка для наследования Dog extends Animal:

rex (экземпляр Dog)
{ name: "Рекс", breed: "Лабрадор" }
[[Prototype]] → Dog.prototype
↓
Dog.prototype
{ bark(), fetch() }
[[Prototype]] → Animal.prototype
↓
Animal.prototype
{ eat(), sleep(), describe() }
[[Prototype]] → Object.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 вопросов

Object.create и цепочка прототипов

// Создайте объект animalMethods с методом speak(), // который возвращает строку с именем животного. // Затем используйте Object.create, чтобы создать объект dog // с прототипом animalMethods и свойством name = 'Рекс'. // // Проверка: dog.speak() должен вернуть 'Рекс издаёт звук' const animalMethods = { speak() { return this.name + ' издаёт звук'; } }; // Создайте dog через Object.create // Ваш код здесь

hasOwnProperty и прототипная цепочка

// У вас есть объект user с собственными свойствами name и email. // Создайте объект admin, прототипом которого будет user. // admin должен иметь собственное свойство role. // // Проверка: // - admin.name должен быть из прототипа user // - admin.role должен быть собственным // - admin.hasOwnProperty('name') === false // - admin.hasOwnProperty('role') === true const user = { name: 'Admin', email: 'admin@mail.com' }; // Создайте admin через Object.create // Ваш код здесь