Свои классы на прототипах

Используем ту же структуру, что JavaScript использует внутри себя, для объявления своих классов.

Обычный конструктор

Вспомним, как мы объявляли классы ранее.

Например, этот код задаёт класс Animal в функциональном стиле, без всяких прототипов:

function Animal(name) {
  this.speed = 0;
  this.name = name;

  this.run = function(speed) {
    this.speed += speed;
    alert( this.name + ' бежит, скорость ' + this.speed );
  };

  this.stop = function() {
    this.speed = 0;
    alert( this.name + ' стоит' );
  };
};

var animal = new Animal('Зверь');

alert( animal.speed ); // 0, начальная скорость
animal.run(3); // Зверь бежит, скорость 3
animal.run(10); // Зверь бежит, скорость 13
animal.stop(); // Зверь стоит

Класс через прототип

А теперь создадим аналогичный класс, используя прототипы, наподобие того, как сделаны классы Object, Date и остальные.

Чтобы объявить свой класс, нужно:

  1. Объявить функцию-конструктор.
  2. Записать методы и свойства, нужные всем объектам класса, в prototype.

Опишем класс Animal:

// конструктор
function Animal(name) {
  this.name = name;
  this.speed = 0;
}

// методы в прототипе
Animal.prototype.run = function(speed) {
  this.speed += speed;
  alert( this.name + ' бежит, скорость ' + this.speed );
};

Animal.prototype.stop = function() {
  this.speed = 0;
  alert( this.name + ' стоит' );
};

var animal = new Animal('Зверь');

alert( animal.speed ); // 0, свойство взято из прототипа
animal.run(5); // Зверь бежит, скорость 5
animal.run(5); // Зверь бежит, скорость 10
animal.stop(); // Зверь стоит

В объекте animal будут хранится свойства конкретного экземпляра: name и speed, а общие методы — в прототипе.

Совершенно такой же подход, как и для встроенных классов в JavaScript.

Сравнение

Чем такое задание класса лучше и хуже функционального стиля?

    Достоинства
  • Функциональный стиль записывает в каждый объект и свойства и методы, а прототипный — только свойства. Поэтому прототипный стиль — быстрее и экономнее по памяти.
    Недостатки
  • При создании методов через прототип, мы теряем возможность использовать локальные переменные как приватные свойства, у них больше нет общей области видимости с конструктором.

Таким образом, прототипный стиль — быстрее и экономнее, но немного менее удобен.

К примеру, есть у нас приватное свойство name и метод sayHi в функциональном стиле ООП:

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

var animal = new Animal("Зверь");
animal.sayHi(); // Зверь

При задании методов в прототипе мы не сможем её так оставить, ведь методы находятся вне конструктора, у них нет общей области видимости, поэтому приходится записывать name в сам объект, обозначив его как защищённое:

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

Animal.prototype.sayHi = function() {
  alert( this._name );
}

var animal = new Animal("Зверь");
animal.sayHi(); // Зверь

Впрочем, недостаток этот — довольно условный. Ведь при наследовании в функциональном стиле также пришлось бы писать this._name, чтобы потомок получил доступ к этому значению.

Задачи

важность: 5
function CoffeeMachine(power) {
  // свойства конкретной кофеварки
  this._power = power;
  this._waterAmount = 0;
}

// свойства и методы для всех объектов класса
CoffeeMachine.prototype.WATER_HEAT_CAPACITY = 4200;

CoffeeMachine.prototype._getTimeToBoil = function() {
  return this._waterAmount * this.WATER_HEAT_CAPACITY * 80 / this._power;
};

CoffeeMachine.prototype.run = function() {
  setTimeout(function() {
    alert( 'Кофе готов!' );
  }, this._getTimeToBoil());
};

CoffeeMachine.prototype.setWaterAmount = function(amount) {
  this._waterAmount = amount;
};

var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.setWaterAmount(50);
coffeeMachine.run();

Есть класс CoffeeMachine, заданный в функциональном стиле.

Задача: переписать CoffeeMachine в виде класса с использованием прототипа.

Исходный код:

function CoffeeMachine(power) {
  var waterAmount = 0;

  var WATER_HEAT_CAPACITY = 4200;

  function getTimeToBoil() {
    return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
  }

  this.run = function() {
    setTimeout(function() {
      alert( 'Кофе готов!' );
    }, getTimeToBoil());
  };

  this.setWaterAmount = function(amount) {
    waterAmount = amount;
  };

}

var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.setWaterAmount(50);
coffeeMachine.run();

P.S. При описании через прототипы локальные переменные недоступны методам, поэтому нужно будет переделать их в защищённые свойства.

важность: 5

Почему возникает проблема

Давайте подробнее разберем происходящее при вызове speedy.found("яблоко"):

  1. Интерпретатор ищет свойство found в speedy. Но speedy — пустой объект, т.к. new Hamster ничего не делает с this.
  2. Интерпретатор идёт по ссылке speedy.__proto__ (==Hamster.prototype) и находят там метод found, запускает его.
  3. Значение this устанавливается в объект перед точкой, т.е. в speedy.
  4. Для выполнения this.food.push() нужно найти свойство this.food. Оно отсутствует в speedy, но есть в speedy.__proto__.
  5. Значение "яблоко" добавляется в speedy.__proto__.food.

У всех хомяков общий живот! Или, в терминах JavaScript, свойство food изменяется в прототипе, который является общим для всех объектов-хомяков.

Заметим, что этой проблемы не было бы при простом присваивании:

this.food = something;

В этом случае значение записалось бы в сам объект, без поиска found в прототипе.

Проблема возникает только со свойствами-объектами в прототипе.

Исправьте её?

Исправление

Для исправления проблемы нужно дать каждому хомяку свой живот. Это можно сделать, присвоив его в конструкторе.

function Hamster() {
  this.food = [];
}

Hamster.prototype.found = function(something) {
  this.food.push(something);
};

speedy = new Hamster();
lazy = new Hamster();

speedy.found("яблоко");
speedy.found("орех");

alert(speedy.food.length) // 2
alert(lazy.food.length) // 0(!)

Теперь всё в порядке. У каждого хомяка — свой живот.

Вы — руководитель команды, которая разрабатывает игру, хомяковую ферму. Один из программистов получил задание создать класс «хомяк» (англ – "Hamster").

Объекты-хомяки должны иметь массив food для хранения еды и метод found, который добавляет к нему.

Ниже — его решение. При создании двух хомяков, если поел один — почему-то сытым становится и второй тоже.

В чём дело? Как поправить?

function Hamster() {}

Hamster.prototype.food = []; // пустой "живот"

Hamster.prototype.found = function(something) {
  this.food.push(something);
};

// Создаём двух хомяков и кормим первого
var speedy = new Hamster();
var lazy = new Hamster();

speedy.found("яблоко");
speedy.found("орех");

alert( speedy.food.length ); // 2
alert( lazy.food.length ); // 2 (!??)
Карта учебника

Комментарии

перед тем как писать…
  • You're welcome to post additions, questions to the articles and answers to them.
  • To insert a few words of code, use the <code> tag, for several lines – use <pre>, for more than 10 lines – use a sandbox (plnkr, JSBin, codepen…)
  • If you can't understand something in the article – please elaborate.