Свойство F.prototype и создание объектов через new

До этого момента мы говорили о наследовании объектов, объявленных через {...}.

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

Свойство F.prototype

Самым очевидным решением является назначение __proto__ в конструкторе.

Например, если я хочу, чтобы у всех объектов, которые создаются new Rabbit, был прототип animal, я могу сделать так:

var animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
  this.__proto__ = animal;
}

var rabbit = new Rabbit("Кроль");

alert( rabbit.eats ); // true, из прототипа

Недостаток этого подхода – он не работает в IE10-.

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

Чтобы новым объектам автоматически ставить прототип, конструктору ставится свойство prototype.

При создании объекта через new, в его прототип __proto__ записывается ссылка из prototype функции-конструктора.

Например, код ниже полностью аналогичен предыдущему, но работает всегда и везде:

var animal = {
  eats: true
};

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

Rabbit.prototype = animal;

var rabbit = new Rabbit("Кроль"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

Установка Rabbit.prototype = animal буквально говорит интерпретатору следующее: "При создании объекта через new Rabbit запиши ему __proto__ = animal".

Свойство prototype имеет смысл только у конструктора

Свойство с именем prototype можно указать на любом объекте, но особый смысл оно имеет, лишь если назначено функции-конструктору.

Само по себе, без вызова оператора new, оно вообще ничего не делает, его единственное назначение – указывать __proto__ для новых объектов.

Значением prototype может быть только объект

Технически, в это свойство можно записать что угодно.

Однако, при работе new, свойство prototype будет использовано лишь в том случае, если это объект. Примитивное значение, такое как число или строка, будет проигнорировано.

Свойство constructor

У каждой функции по умолчанию уже есть свойство prototype.

Оно содержит объект такого вида:

function Rabbit() {}

Rabbit.prototype = {
  constructor: Rabbit
};

В коде выше я создал Rabbit.prototype вручную, но ровно такой же – генерируется автоматически.

Проверим:

function Rabbit() {}

// в Rabbit.prototype есть одно свойство: constructor
alert( Object.getOwnPropertyNames(Rabbit.prototype) ); // constructor

// оно равно Rabbit
alert( Rabbit.prototype.constructor == Rabbit ); // true

Можно его использовать для создания объекта с тем же конструктором, что и данный:

function Rabbit(name) {
  this.name = name;
  alert( name );
}

var rabbit = new Rabbit("Кроль");

var rabbit2 = new rabbit.constructor("Крольчиха");

Эта возможность бывает полезна, когда, получив объект, мы не знаем в точности, какой у него был конструктор (например, сделан вне нашего кода), а нужно создать такой же.

Свойство constructor легко потерять

JavaScript никак не использует свойство constructor. То есть, оно создаётся автоматически, а что с ним происходит дальше – это уже наша забота. В стандарте прописано только его создание.

В частности, при перезаписи Rabbit.prototype = { jumps: true } свойства constructor больше не будет.

Сам интерпретатор JavaScript его в служебных целях не требует, поэтому в работе объектов ничего не «сломается». Но если мы хотим, чтобы возможность получить конструктор, всё же, была, то можно при перезаписи гарантировать наличие constructor вручную:

Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

Либо можно поступить аккуратно и добавить свойства к встроенному prototype без его замены:

// сохранится встроенный constructor
Rabbit.prototype.jumps = true

Эмуляция Object.create для IE8-

Как мы только что видели, с конструкторами всё просто, назначить прототип можно кросс-браузерно при помощи F.prototype.

Теперь небольшое «лирическое отступление» в область совместимости.

Прямые методы работы с прототипом отсутствуют в старых IE, но один из них – Object.create(proto) можно эмулировать, как раз при помощи prototype. И он будет работать везде, даже в самых устаревших браузерах.

Кросс-браузерный аналог – назовём его inherit, состоит буквально из нескольких строк:

function inherit(proto) {
  function F() {}
  F.prototype = proto;
  var object = new F;
  return object;
}

Результат вызова inherit(animal) идентичен Object.create(animal). Она создаёт новый пустой объект с прототипом animal.

Например:

var animal = {
  eats: true
};

var rabbit = inherit(animal);

alert( rabbit.eats ); // true

Посмотрите внимательно на функцию inherit и вы, наверняка, сами поймёте, как она работает…

Если где-то неясности, то её построчное описание:

function inherit(proto) {
  function F() {}     // (1)
  F.prototype = proto // (2)
  var object = new F; // (3)
  return object;      // (4)
}
  1. Создана новая функция F. Она ничего не делает с this, так что если вызвать new F, то получим пустой объект.
  2. Свойство F.prototype устанавливается в будущий прототип proto
  3. Результатом вызова new F будет пустой объект с __proto__ равным значению F.prototype.
  4. Мы получили пустой объект с заданным прототипом, как и хотели. Возвратим его.

Для унификации можно запустить такой код, и метод Object.create станет кросс-браузерным:

if (!Object.create) Object.create = inherit; /* определение inherit - выше */

В частности, аналогичным образом работает библиотека es5-shim, при подключении которой Object.create станет доступен для всех браузеров.

Итого

Для произвольной функции – назовём её Person, верно следующее:

  • Прототип __proto__ новых объектов, создаваемых через new Person, можно задавать при помощи свойства Person.prototype.
  • Значением Person.prototype по умолчанию является объект с единственным свойством constructor, содержащим ссылку на Person. Его можно использовать, чтобы из самого объекта получить функцию, которая его создала. Однако, JavaScript никак не поддерживает корректность этого свойства, поэтому программист может его изменить или удалить.
  • Современный метод Object.create(proto) можно эмулировать при помощи prototype, если хочется, чтобы он работал в IE8-.

Задачи

важность: 5

В примерах ниже создаётся объект new Rabbit, а затем проводятся различные действия с prototype.

Каковы будут результаты выполнения? Почему?

Начнём с этого кода. Что он выведет?

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

var rabbit = new Rabbit();

alert( rabbit.eats );

Добавили строку (выделена), что будет теперь?

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

var rabbit = new Rabbit();

Rabbit.prototype = {};

alert( rabbit.eats );

А если код будет такой? (заменена одна строка):

function Rabbit(name) {}
Rabbit.prototype = {
  eats: true
};

var rabbit = new Rabbit();

Rabbit.prototype.eats = false;

alert( rabbit.eats );

А такой? (заменена одна строка)

function Rabbit(name) {}
Rabbit.prototype = {
  eats: true
};

var rabbit = new Rabbit();

delete rabbit.eats; // (*)

alert( rabbit.eats );

И последний вариант:

function Rabbit(name) {}
Rabbit.prototype = {
  eats: true
};

var rabbit = new Rabbit();

delete Rabbit.prototype.eats; // (*)

alert( rabbit.eats );

Результат: true, из прототипа

Результат: true. Свойство prototype всего лишь задаёт __proto__ у новых объектов. Так что его изменение не повлияет на rabbit.__proto__. Свойство eats будет получено из прототипа.

Результат: false. Свойство Rabbit.prototype и rabbit.__proto__ указывают на один и тот же объект. В данном случае изменения вносятся в сам объект.

Результат: true, так как delete rabbit.eats попытается удалить eats из rabbit, где его и так нет. А чтение в alert произойдёт из прототипа.

Результат: undefined. Удаление осуществляется из самого прототипа, поэтому свойство rabbit.eats больше взять неоткуда.

важность: 4

Есть функция Menu, которая получает аргументы в виде объекта options:

/* options содержит настройки меню: width, height и т.п. */
function Menu(options) {
  ...
}

Ряд опций должны иметь значение по умолчанию. Мы могли бы проставить их напрямую в объекте options:

function Menu(options) {
  options.width = options.width || 300; // по умолчанию ширина 300
  ...
}

…Но такие изменения могут привести к непредвиденным результатам, т.к. объект options может быть повторно использован во внешнем коде. Он передается в Menu для того, чтобы параметры из него читали, а не писали.

Один из способов безопасно назначить значения по умолчанию – скопировать все свойства options в локальные переменные и затем уже менять. Другой способ – клонировать options путём копирования всех свойств из него в новый объект, который уже изменяется.

При помощи наследования и Object.create предложите третий способ, который позволяет избежать копирования объекта и не требует новых переменных.

Можно прототипно унаследовать от options и добавлять/менять опции в наследнике:

function Menu(options) {
  options = Object.create(options);
  options.width = 300;

  alert("width: " + options.width); // возьмёт width из наследника
  alert("height: " + options.height); // возьмёт height из исходного объекта
}

var options = {
  width: 100,
  height: 200
};

var menu = new Menu(options);

alert("original width: " + options.width); // width исходного объекта
alert("original height: " + options.height); // height исходного объекта

Все изменения будут происходить не в исходном options, а в его наследнике, при этом options останется незатронутым.

важность: 5

Создадим новый объект, вот такой:

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

var rabbit = new Rabbit("Rabbit");

Одинаково ли сработают эти вызовы?

rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();

Все ли они являются кросс-браузерными? Если нет – в каких браузерах сработает каждый?

Разница между вызовами

Первый вызов ставит this == rabbit, остальные ставят this равным Rabbit.prototype, следуя правилу "this – объект перед точкой".

Так что только первый вызов выведет Rabbit, в остальных он будет undefined.

Код для проверки:

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

var rabbit = new Rabbit("Rabbit");

rabbit.sayHi();
Rabbit.prototype.sayHi();
Object.getPrototypeOf(rabbit).sayHi();
rabbit.__proto__.sayHi();

Совместимость

  1. Первый вызов работает везде.
  2. Второй вызов работает везде.
  3. Третий вызов не будет работать в IE8-, там нет метода getPrototypeOf
  4. Четвёртый вызов – самый «несовместимый», он не будет работать в IE10-, ввиду отсутствия свойства __proto__.
важность: 5

Пусть у нас есть произвольный объект obj, созданный каким-то конструктором, каким – мы не знаем, но хотели бы создать новый объект с его помощью.

Сможем ли мы сделать так?

var obj2 = new obj.constructor();

Приведите пример конструкторов для obj, при которых такой код будет работать верно – и неверно.

Да, можем, но только если уверены, что кто-то позаботился о том, чтобы значение constructor было верным.

В частности, без вмешательства в прототип код точно работает, например:

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

var obj = new User('Вася');
var obj2 = new obj.constructor('Петя');

alert( obj2.name ); // Петя (сработало)

Сработало, так как User.prototype.constructor == User.

Но если кто-то, к примеру, перезапишет User.prototype и забудет указать constructor, то такой фокус не пройдёт, например:

function User(name) {
    this.name = name;
  }
User.prototype = {}; // (*)

var obj = new User('Вася');
var obj2 = new obj.constructor('Петя');

alert( obj2.name ); // undefined

Почему obj2.name равен undefined? Вот как это работает:

  1. При вызове new obj.constructor('Петя'), obj ищет у себя свойство constructor – не находит.
  2. Обращается к своему свойству __proto__, которое ведёт к прототипу.
  3. Прототипом будет (*), пустой объект.
  4. Далее здесь также ищется свойство constructor – его нет.
  5. Где ищем дальше? Правильно – у следующего прототипа выше, а им будет Object.prototype.
  6. Свойство Object.prototype.constructor существует, это встроенный конструктор объектов, который, вообще говоря, не предназначен для вызова с аргументом-строкой, поэтому создаст совсем не то, что ожидается, но то же самое, что вызов new Object('Петя'), и у такого объекта не будет name.
Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.