Объектно-ориентированный JavaScript: прототипы

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

console.dir([].__proto__);
// То же самое, что и
console.dir(Array.prototype);

Разумеется, мы сами можем создавать функции конструкторы и задавать им прототипы. Итак, вы являетесь счастливым владельцем зоопарка. Для каждого животного вы хотите создать объект, содержащий вид и кличку животного. Каждое животное также может издавать звуки. Функция-конструктор в данном случае будет выглядеть следующим образом:

const Animal = function(name, species, sound) {
  this.name = name;
  this.species = species;
  this.sound = sound;
};

Animal.prototype.speak = function() {
  return this.species + ' ' + this.name + ' says ' + this.sound + '!';
};

Теперь мы можем создать один или несколько объектов с помощью функции конструктора Animal и оператора new:

const cat = new Animal('Wizard', 'Cat', 'Meow');
const dog = new Animal('Pancho', 'Dog', 'Woof');
const fox = new Animal('Oliver', 'Fox', '????');

Каждый созданный нами объект не содержит своего метода speak. В этом вы можете убедиться, просто выведя объект в консоль console.dir(cat). Тем не менее, все созданные объекты могут использовать метод speak:

console.log(dog.speak()); // Dog Pancho says Woof!
console.log(cat.speak()); // Cat Wizard says Meow!
console.log(fox.speak()); // Fox Oliver says ????!

Что происходит на самом деле. Когда вы используете метод speak с объектом, сначала происходит проверка того, есть ли у самого объекта этот метод. Если метода нет, то далее следует проверка на присутствие метода в прототипе. Если метода нет и в прототипе, то метод может быть найден в прототипе прототипа. И так далее, пока выполнение не дойдёт до последнего прототипа, который всегда содержит в себе все методы функции-конструктора Object. Чтобы в этом убедиться попробуйте выполнить в консоле браузера несколько строчек кода:

// Прототипы для чисел
console.dir((10).__proto__); // Number
console.dir((10).__proto__.__proto__); // Object

// Прототипы для строк
console.dir('str'.__proto__); // String
console.dir('str'.__proto__.__proto__); // Object

// Прототипы для объектов
console.dir([].__proto__); // Array
console.dir([].__proto__.__proto__); // Object

// Прототипы для созданной нами функции-конструктора Animal
console.dir(cat.__proto__); // Animal
console.dir(cat.__proto__.__proto__); // Object

Таким образом, созданный нами объект cat унаследовал все методы не только от функции Animal, но и от Object. В этом легко убедиться с помощью использования любого метода объектов, например, toString():

console.log(cat.toString()); // "[object Object]"

Как я уже писал выше, при использовании любого метода сначала проверяется его наличие в самом объекте. Поэтому мы может сами переназначить тот же метод toString для отдельного объекта cat:

const cat = new Animal('Wizard', 'Cat', 'Meow');
const dog = new Animal('Pancho', 'Dog', 'Woof');

cat.toString = function() {
  return this.species + ' ' + this.name;
};

console.log(cat.toString()); // Cat Wizard
console.log(dog.toString()); // "[object Object]"

Или же можно записать данный метод в прототип Animal, чтобы все создаваемые с помощью этой функции-конструктора объекты использовали именно его:

Animal.prototype.toString = function() {
  return 'This is ' + this.species + ' ' + this.name;
};

cat.toString = function() {
  return this.species + ' ' + this.name;
};

const cat = new Animal('Wizard', 'Cat', 'Meow');
const dog = new Animal('Pancho', 'Dog', 'Woof');
const fox = new Animal('Oliver', 'Fox', '????');

console.log(cat.toString()); // Cat Wizard
console.log(dog.toString()); // This is Dog Pancho
console.log(fox.toString()); // This is Fox Oliver

Теперь объекты dog и fox обращаются к прототипу Animal, а объект cat имеет собственный метод и использует его.

Есть два способа устанавливать свойства в прототипы для объектов: плохой и хороший. Хороший способ вы уже видели — все примеры приведённые выше написаны с его помощью.

Animal.prototype.speak = function() { /* code here */ };
Animal.prototype.toString = function() { /* code here */ };

В данном примере к прототипу просто добавляется ещё одно свойство, в независимости от того, что было в прототипе было раньше.

Пример плохого добавления свойств в прототип — непосредственная его перезапись новым объектом:

Animal.prototype.toString = function() { /* code here */ };
// ...
Animal.prototype = {
  speak: function() {}
};

Почему так делать плохо? Всё просто. Перезаписывая прототип новым объектом, вы полностью стираете все методы, которые были записаны в него ранее. Поэтому в приведённом выше примере метода toString у всех объектов, созданных с помощью функции-конструктора Animal, не будет.

Комментарии