Прототипы

Прототип — объект, от которого другой объект наследует свойства и методы. Каждый объект в JavaScript имеет внутреннюю ссылку [[Прототипы]] на свой прототип, образуя прототипную цепочку.

Зачем нужно

Прототипы — основа наследования в JavaScript. Даже классы ES6 — синтаксический сахар над прототипами. Понимание прототипной цепочки объясняет, как работает instanceof, как методы наследуются, и почему toString доступен у любого объекта.

Где используется

Наследование, расширение встроенных объектов, полифиллы, метод-шеринг между экземплярами, проверка типов, миксины.

Предпосылки

this, Конструкторы

Прототипы и proto

const animal = {
  eats: true,
  walk {
    console.log('Животное идёт');
  }
};

const rabbit = {
  jumps: true,
  __proto__: animal // устанавливаем прототип
};

console.log(rabbit.jumps); // true — собственное свойство
console.log(rabbit.eats);  // true — унаследовано от animal
rabbit.walk;              // "Животное идёт" — метод из прототипа

Object.getPrototypeOf / Object.setPrototypeOf

// Рекомендуемый способ (вместо __proto__)
const proto = { greet { return 'Привет'; } };
const obj = Object.create(proto);

console.log(Object.getPrototypeOf(obj) === proto); // true
console.log(obj.greet); // "Привет"

// Изменение прототипа (медленная операция, избегать)
Object.setPrototypeOf(obj, null);
// obj.greet; // TypeError — прототип удалён

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

const grandparent = { family: 'Ивановы' };
const parent = Object.create(grandparent);
parent.role = 'родитель';
const child = Object.create(parent);
child.name = 'Маша';

console.log(child.name);   // 'Маша' — собственное
console.log(child.role);   // 'родитель' — от parent
console.log(child.family); // 'Ивановы' — от grandparent
console.log(child.toString()); // function — от Object.prototype

// Цепочка: child → parent → grandparent → Object.prototype → null

Конец цепочки

console.log(Object.getPrototypeOf(Object.prototype)); // null
// Object.prototype — вершина цепочки

prototype у функций-конструкторов

function User(name) {
  this.name = name;
}

// Методы в prototype — общие для всех экземпляров
User.prototype.greet = function {
  return `Привет, ${this.name}`;
};

User.prototype.role = 'user';

const ivan = new User('Иван');
const maria = new User('Мария');

console.log(ivan.greet);  // "Привет, Иван"
console.log(maria.greet); // "Привет, Мария"

// Один и тот же метод
console.log(ivan.greet === maria.greet); // true — экономия памяти

// Цепочка: ivan → User.prototype → Object.prototype → null
console.log(Object.getPrototypeOf(ivan) === User.prototype); // true

constructor

console.log(User.prototype.constructor === User); // true
console.log(ivan.constructor === User);            // true (через цепочку)

Object.create

// Создание объекта с указанным прототипом
const personProto = {
  greet {
    return `Привет, я ${this.name}`;
  },
  toString {
    return `[Person: ${this.name}]`;
  }
};

const alice = Object.create(personProto);
alice.name = 'Алиса';
console.log(alice.greet); // "Привет, я Алиса"

// Создание с дескрипторами свойств
const bob = Object.create(personProto, {
  name: { value: 'Боб', writable: true, enumerable: true },
  age: { value: 30, writable: false }
});

// Объект без прототипа (чистый словарь)
const dict = Object.create(null);
dict.key = 'value';
console.log(dict.toString()); // undefined — нет Object.prototype

Поиск свойств в цепочке

const base = { x: 10, y: 20 };
const derived = Object.create(base);
derived.x = 100; // Затеняет base.x

console.log(derived.x); // 100 — собственное (shadow)
console.log(derived.y); // 20  — из прототипа

// Проверка: собственное свойство или унаследованное
console.log(derived.hasOwnProperty('x')); // true
console.log(derived.hasOwnProperty('y')); // false

// Оператор in проверяет и цепочку
console.log('x' in derived); // true
console.log('y' in derived); // true

// Только собственные ключи
console.log(Object.keys(derived));          // ['x']
console.log(Object.getOwnPropertyNames(derived)); // ['x']

Запись свойств

const proto = {
  get value { return this._value; },
  set value(v) { this._value = v; }
};

const obj = Object.create(proto);
obj.value = 42;

console.log(obj._value);       // 42 — записано в obj (через setter)
console.log(obj.hasOwnProperty('_value')); // true
console.log(obj.hasOwnProperty('value'));  // false — геттер/сеттер в прототипе

Перебор с учётом прототипов

const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;

// for...in перебирает и унаследованные
for (const key in rabbit) {
  console.log(key); // jumps, eats
}

// Только собственные
for (const key in rabbit) {
  if (rabbit.hasOwnProperty(key)) {
    console.log(key); // только jumps
  }
}

// Object.keys() — только собственные
console.log(Object.keys(rabbit)); // ['jumps']

Встроенные прототипы

// Все встроенные типы имеют прототипы
const arr = [1, 2, 3];
// arr → Array.prototype → Object.prototype → null

const str = 'hello';
// str (обёрнут) → String.prototype → Object.prototype → null

const fn = function {};
// fn → Function.prototype → Object.prototype → null

// Поэтому у массива есть map, у строки — slice и т.д.
console.log(arr.__proto__ === Array.prototype);    // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true

Частые ошибки

1. Перезапись prototype целиком

function User(name) { this.name = name; }
User.prototype.greet = function { return this.name; };

// Неправильно — теряем constructor и старые методы
User.prototype = {
  sayHi { return 'Hi'; }
};

const user = new User('Иван');
console.log(user.constructor === User); // false!

// Правильно — добавляем метод
User.prototype.sayHi = function { return 'Hi'; };
// Или восстанавливаем constructor
User.prototype = {
  constructor: User,
  sayHi { return 'Hi'; }
};

2. Мутация прототипа — влияет на все экземпляры

function Item() {}
Item.prototype.tags = ; // Общий массив!

const a = new Item();
const b = new Item();
a.tags.push('test');
console.log(b.tags); // ['test'] — тоже изменился!

// Решение: инициализировать в конструкторе
function ItemFixed() {
  this.tags = ; // Свой массив у каждого
}

3. Использование proto в продакшене

// __proto__ — устаревший способ, не для продакшена
// Используйте Object.create, Object.getPrototypeOf

Практика

  1. Создай цепочку прототипов: animal → dog → puppy и проверь наследование
  2. Добавь метод в Array.prototype и вызови его на массиве
  3. Создай объект через Object.create(null) — убедись, что toString отсутствует
  4. Реализуй наследование через функции-конструкторы и prototype
  5. Напиши функцию instanceOf(obj, Constructor), проверяющую цепочку прототипов

Ключевые тезисы из лекций

  • «Любая функция (не лямбда) в JavaScript сразу же имеет свойство prototype. Это пустой объект, который служит прототипом будущих инстансов» (автор, SzaXTW2qcJE).
  • Циклическая ссылка constructor↔prototype. Point.prototype.constructor === Point. Это цикл, который НЕ влияет на цепочку прототипов (она идёт через __proto__).
  • Статика на конструкторе. Point.from = ... — метод на функции, доступен только через имя класса. В class статика помечена как not enumerable.
  • Двойственная природа классов. class помечен внутренне как kind: class — нельзя вызвать без new (в отличие от функции-конструктора).

Связанные темы

Источники