Свойство prototype

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

function Rect() {
         this.name = "прямоугольник";
 
         this.draw = function() {
                   console.log("Рисование фигуры: "+this.name);
         }
}
 
let r = new Rect();
r.draw();

Как здесь указывать базовый объект, который бы расширяла функция Rect? Для этого также можно воспользоваться свойством __proto__, о котором мы начали говорить на предыдущем занятии.

Допустим, выше имеется объявление объекта prop:

let prop = {
         sp: {x: 0, y: 0},
         ep: {x: 100, y: 20},
         get coords() {
                   return [this.sp.x, this.sp.y, this.ep.x, this.ep.y];
         },
         set coords(coords) {
                   this.sp.x = coords[0]; this.sp.y = coords[1];
                   this.ep.x = coords[2]; this.ep.y = coords[3];
         }
};

и, далее, в функции добавим строчку:

this.__proto__ = prop;

Все, мы установили в качестве базового объекта – объект prop. В этом легко убедиться, если перебрать свойства объекта r:

for(let p in r)
         console.log(p+': '+r[p]);

Как видите, в консоли отобразились атрибуты, связанные и с объектом prop. Однако, в ранних скриптах можно встретить и другую запись:

Rect.prototype = prop;

Причем, обращение к свойству prototype происходит вне функции. Это имеет принципиальное значение. Если поместить эту конструкцию внутрь:

this.prototype = prop;

то будет попросту создано свойство с таким именем и значением ссылки на объект prop. Получается, что prototype – это не совсем прототип объекта, подобно другому свойству __proto__, которое можно использовать и после создания объекта. Здесь же важно, чтобы мы определили свойство prototype до вызова оператора new. Тогда в процессе создания объекта Rect объект, указанный в prototype автоматически станет базовым для Rect. Мало того, в таком качестве свойство prototype используется только в момент вызова оператора new. В последующем, его можно совершенно спокойно видоизменять и это никак не скажется на связке «базовый объект – дочерний объект». Например, после создания объекта r, изменим это свойство:

r.prototype = "hello"

и при выводе свойств для r видим в консоли все атрибуты базового объекта и еще свойство prototype со значением hello. То есть, механизм наследования никак не был затронут после создания объекта r и изменения prototype.

Связь между __proto__ и prototype

Поведение свойств __proto__ и prototype очень похоже, следовательно, между ними должна быть какая-то связь. И, действительно, __proto__ это аксессор объекта prototype:

То есть, когда встречается конструкция:

this.__proto__ = prop;

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

Rect.prototype.__proto__ = prop;

и тогда в момент создания текущему объекту будет назначен базовый prop. Причем, автоматически будет создан некий базовый объект для r, в котором через свойство __proto__ устанавливается связь на объект prop:

Добавление свойств через prototype

Итак, мы только что увидели, что поведение свойства prototype несколько более изощренное, чем можно было подумать на первый взгляд. Например, зададимся вопросом: что будет, если в этом объекте создать новые свойства:

Rect.prototype.__proto__ = prop;
Rect.prototype.getName = function() { return this.name; }
Rect.prototype.setName = function(name) { this.name = name; }

То есть, теперь prototype ссылается не на какой-то конкретный объект, а являясь сам по себе объектом, приобретет дополнительные свойства: prop, getName и setName. В этом случае, новый созданный объект Rect будет иметь доступ ко всем этими свойствам, расположенные в базовом объекте, а базовый объект, в свою очередь, будет ссылаться на объект prop:

let r = new Rect();
console.log( r.prop.coords );
console.log( r.getName() );

Свойство constructor

Каждая функция в JavaScript уже хранит в объекте prototype некоторую служебную информацию. Например, если выполнить строчку:

console.log(Rect.prototype)

то мы увидим в этом объекте свойства:

  • constructor – ссылка на функцию-конструктор для создания объекта;
  • __proto__ – объект, определяющий объект-прототип.

Поэтому, изменяя его:

Rect.prototype = prop;

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

console.log(r.constructor == Rect)    //false

и после комментирования строки:

// Rect.prototype = prop;

Увидим совершенно разные результаты: false и true. Чтобы выполнить наследование объектов с использованием prototype и сохранить информацию о конструкторе, можно переопределить это свойство при создании объекта, то есть, внутри функции Rect записать строчку:

Rect.prototype.constructor = Rect;

Тем самым мы восстановим это значение. Но зачем оно вообще нужно? Теряется и пусть теряется. Дело в том, что в больших и сложных программах может потребоваться создать подобный объект. Здесь как раз хорошо подходит это свойство:

let r2 = new r.constructor();

В ряде случаев, такой подход очень удобен. И чтобы иметь эту функциональность, мы и сохраняем свойство constructor в prototype.