Прототипы


Прототипы в JavaScript существовали с самого момента его создания.
Идея прототипов следующая.
Представим, что у нас есть несколько видов животных, у каждого из которых есть функция say(), которая воспроизводит голос этого животного.

Но, если разные объекты имеют одну и ту же функцию, есть ли смысл хранить её внутри каждого такого объекта? Или лучше создать общий для них родительский объект, поместить в него данную функцию и дать остальным объектам доступ к ней?

Этот родительский объект, хранящий функции всех его потомков, называется прототипом. Правило обращения к методам прототипа следующее. Вначале js-движок ищет функцию внутри самого объекта, если не находит - ищет в его прототипе.
Но прототип - тоже объект и у него есть собственный прототип.
Таким образом js-движок в поисках вызванной функции проходит по всей цепочке прототипов


Перейдём от теории к коду.
Вначале напишем код  животных, у каждого из которых есть функция say()
Прототипов пока нет

const dog = {
  name: 'dog',
  voice: 'woof',
  say: function() {
    console.log(this.name, 'says', this.voice)
  }
}

const cat = {
  name: 'cat',
  voice: 'meow',
  say: function() {
    console.log(this.name, 'says', this.voice)
  }
}
dog.say();   //  dog says woof
cat.say();   //   cat says meow

Вынесем функцию say() в объект animal:

const animal = { 
  say: function() {
    console.log(this.name, 'says', this.voice)
  }
}

const dog = {
  name: 'dog',
  voice: 'woof'
}

const cat = {
  name: 'cat',
  voice: 'meow'
}

dog.say();
cat.say();

Ожидаемо получаем ошибку: объекты dog и cat никак не связаны с объектом animal и ничего не знают о хранящихся в нём методах.

Попробуем связать эта объекты и указать. что animal является прототипом для dog и cat

1-й способ

Используем появившуюся в 2015 году функцию Object.setPrototypeOf, которая позволяет связать объект и его прототип

const animal = { 
  say: function() {
    console.log(this.name, 'says', this.voice)
  }
}

const dog = {
  name: 'dog',
  voice: 'woof'
}
Object.setPrototypeOf(dog, animal)

const cat = {
  name: 'cat',
  voice: 'meow'
}
Object.setPrototypeOf(cat, animal)

dog.say();   //  dog says woof
cat.say();   //   cat says meow

Всё работает. Проблема в том, что Object.setPrototypeOf очень плохо влияет на производительность и использовать её не рекомендуется

2-й способ

Используем Object.create()

const animal = {  
  say: function() {
    console.log(this.name, 'says', this.voice)
  }
}

const dog = Object.create(animal);
dog.name = 'dog';
dog.voice = 'woof';
dog.say();    //  dog says woof

Строка const dog = Object.create(animal); означает, что мы создаём объект dog при помощи метода Object.create(). Прототипом созданного объекта dog будет объект animal.

3-й способ

Функция-конструктор new

Сделаем предыдущий код чуть лучше, вынесем функцию создания объекта и прототипа в отдельную функцию createAnimal

const animal = {  
  say: function() {
    console.log(this.name, 'says', this.voice)
  }
}

function createAnimal(name, voice) {
  const result = Object.create(animal);
  result.name = name;
  result.voice = voice;
  return result;
}

const dog = createAnimal('dog', 'woof');
dog.say();     //  dog says woof

Функция createAnimal - это функция-конструктор, при помощи которой можно удобно и быстро создавать новых животных.
Но такая функция в JavaScript уже есть. для её вызова используется ключевое слово new

Сравним функции createAnimal и new Animal

function Animal(name, voice) {
  this.name = name;
  this.voice = voice;
}

Animal.prototype.say = function() {
    console.log(this.name, 'says', this.voice)
  }

const dog = new Animal('dog', 'woof');
dog.say();    //  dog says woof

Такой код неплохо работает и использовался с самых первых дней создания JavaScript. Дальше пойдёт речь о классах, которые являются синтаксическим сахаром, семантической обёрткой над прототипами и позволяют их проще и удобнее использовать.

Да, если вдруг понадобится создать объект без прототипа, это удобно делать при помощи Object.create(null)

const obj = Object.create(null);
console.log(obj.toString())   //   TypeError