Avatar ·

Как работает .prototype в JavaScript? Объясните на примерах

Всем привет! Я начал изучать JavaScript и никак не могу разобраться с прототипами. Читал документацию, смотрел видео, но всё равно путаюсь. Вот у меня есть такой код: ```javascript function Animal(name) { this.name = name; } Animal.prototype.sayHi = function() { console.log('Привет, я ' + this.name); }; let cat = new Animal('Барсик'); cat.sayHi(); // "Привет, я Барсик" ``` Я понимаю, что `sayHi` появляется у всех объектов, созданных через `new Animal`. Но зачем вообще нужен `.prototype`? Почему нельзя просто добавить метод прямо в конструктор? ```javascript function Animal(name) { this.name = name; this.sayHi = function() { console.log('Привет, я ' + this.name); }; } ``` В чём разница? И что такое `__proto__`? Я видел, что иногда используют его, а иногда `.prototype`. Это одно и то же? Ещё меня смущает цепочка прототипов. Когда я пишу `cat.toString()`, откуда берётся этот метод, если я его не определял? И как работает `Object.create()`? Объясните, пожалуйста, максимально просто, с примерами. А то я уже голову сломал. <<>>

Ключевое отличие: прототип vs конструктор

Давайте разберёмся с главным вопросом. Когда вы добавляете метод в конструктор:

function Animal(name) {
  this.name = name;
  this.sayHi = function() { ... };
}

Каждый раз при вызове new Animal() создаётся новая копия функции sayHi. Если у вас 1000 животных, в памяти будет 1000 одинаковых функций. Это неэффективно.

Когда вы используете .prototype:

Animal.prototype.sayHi = function() { ... };

Функция существует в единственном экземпляре на прототипе. Все объекты просто ссылаются на неё. Экономия памяти и времени.

Что такое prototype и __proto__

Это разные вещи, хотя они связаны:

  • .prototype — свойство функции-конструктора. Это объект, который станет прототипом для всех созданных через new экземпляров.
  • __proto__ — свойство каждого объекта, которое указывает на его прототип (родителя). Это ссылка на тот самый .prototype конструктора.
function Animal(name) {
  this.name = name;
}

let cat = new Animal('Барсик');

console.log(Animal.prototype); // объект с sayHi
console.log(cat.__proto__);    // тот же объект!
console.log(cat.__proto__ === Animal.prototype); // true

Цепочка прототипов

Когда вы обращаетесь к свойству объекта, JavaScript идёт по цепочке:

  1. Сначала проверяет сам объект
  2. Если не нашёл — идёт в __proto__ (прототип)
  3. Если и там нет — в __proto__.__proto__ и так далее
  4. Пока не дойдёт до null (конец цепочки)

Поэтому cat.toString() работает: его нет у кота и у Animal.prototype, но он есть у Object.prototype (вершина цепочки).

// Цепочка для cat:
cat → Animal.prototype → Object.prototype → null

// Проверка:
console.log(cat.toString); // функция из Object.prototype
console.log(cat.hasOwnProperty('toString')); // false
console.log('toString' in cat); // true (нашёлся в цепочке)

Object.create() и прототипы

Object.create(proto) создаёт новый объект с указанным прототипом:

let animal = {
  eat() { console.log('Ням-ням'); }
};

let rabbit = Object.create(animal);
rabbit.eat(); // "Ням-ням" — унаследовано

console.log(rabbit.__proto__ === animal); // true

Как это работает на практике

Полный пример с наследованием:

// Базовый "класс"
function Animal(name) {
  this.name = name;
}

Animal.prototype.sayHi = function() {
  console.log('Привет, я ' + this.name);
};

// "Наследуем"
function Cat(name, color) {
  Animal.call(this, name); // вызываем родительский конструктор
  this.color = color;
}

// Устанавливаем прототип
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // восстанавливаем constructor

// Добавляем свой метод
Cat.prototype.meow = function() {
  console.log('Мяу!');
};

let barsik = new Cat('Барсик', 'рыжий');
barsik.sayHi(); // "Привет, я Барсик" — из Animal.prototype
barsik.meow();  // "Мяу!" — из Cat.prototype
console.log(barsik.color); // "рыжий" — собственное свойство

Современная альтернатива: class

С ES6 появился синтаксис class, который делает то же самое, но красивее:

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  sayHi() {
    console.log('Привет, я ' + this.name);
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super(name); // вызов родительского конструктора
    this.color = color;
  }
  
  meow() {
    console.log('Мяу!');
  }
}

let barsik = new Cat('Барсик', 'рыжий');

Под капотом это всё те же прототипы. Просто синтаксический сахар.

<<>>

Практический взгляд: когда прототипы реально нужны

Многие новички думают, что прототипы — это архаизм, и можно обойтись без них. На практике вы будете сталкиваться с ними постоянно, даже не осознавая.

1. Расширение встроенных объектов

Хотя это считается плохой практикой, но вы можете добавить метод всем массивам:

Array.prototype.last = function() {
  return this[this.length - 1];
};

console.log([1, 2, 3].last()); // 3

2. Полифиллы (эмуляция новых возможностей в старых браузерах)

Например, если браузер не поддерживает Array.includes:

if (!Array.prototype.includes) {
  Array.prototype.includes = function(element) {
    return this.indexOf(element) !== -1;
  };
}

3. Оптимизация производительности

Когда у вас тысячи однотипных объектов (например, частиц в игре), прототипы дают огромную экономию памяти:

function Particle(x, y) {
  this.x = x;
  this.y = y;
}

// Метод один для всех 10000 частиц
Particle.prototype.move = function(dx, dy) {
  this.x += dx;
  this.y += dy;
};

4. Современные библиотеки

React, Vue, jQuery — все используют прототипы внутри. Когда вы пишете $(selector).hide(), jQuery берёт метод из своего прототипа.

Главный совет: Если вы пишете современный код на ES6+, используйте class — он скрывает работу с прототипами, но понимать их нужно, чтобы разбираться в ошибках и читать чужой код.