Свойство F.prototype и создание объектов через new
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/function-prototype.
До этого момента мы говорили о наследовании объектов, объявленных через <. >.
Но в реальных проектах объекты обычно создаются функцией-конструктором через 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 можно указать на любом объекте, но особый смысл оно имеет, лишь если назначено функции-конструктору.
Само по себе, без вызова оператора new , оно вообще ничего не делает, его единственное назначение – указывать __proto__ для новых объектов.
Технически, в это свойство можно записать что угодно.
Однако, при работе 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("Крольчиха");
Эта возможность бывает полезна, когда, получив объект, мы не знаем в точности, какой у него был конструктор (например, сделан вне нашего кода), а нужно создать такой же.
JavaScript никак не использует свойство constructor . То есть, оно создаётся автоматически, а что с ним происходит дальше – это уже наша забота. В стандарте прописано только его создание.
В частности, при перезаписи Rabbit.prototype = < jumps: true >свойства constructor больше не будет.
Сам интерпретатор JavaScript его в служебных целях не требует, поэтому в работе объектов ничего не «сломается». Но если мы хотим, чтобы возможность получить конструктор, всё же, была, то можно при перезаписи гарантировать наличие constructor вручную:
Либо можно поступить аккуратно и добавить свойства к встроенному 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) >
- Создана новая функция F . Она ничего не делает с this , так что если вызвать new F , то получим пустой объект.
- Свойство F.prototype устанавливается в будущий прототип proto
- Результатом вызова new F будет пустой объект с __proto__ равным значению F.prototype .
- Мы получили пустой объект с заданным прототипом, как и хотели. Возвратим его.
Для унификации можно запустить такой код, и метод 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-.
Задачи
Прототип после создания
В примерах ниже создаётся объект 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 больше взять неоткуда.
Аргументы по умолчанию
Есть функция Menu , которая получает аргументы в виде объекта options :
/* options содержит настройки меню: width, height и т.п. */ function Menu(options)
Ряд опций должны иметь значение по умолчанию. Мы могли бы проставить их напрямую в объекте options :
…Но такие изменения могут привести к непредвиденным результатам, т.к. объект 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 останется незатронутым.
Комментарии
- Если вам кажется, что в статье что-то не так — вместо комментария напишите на GitHub.
- Для одной строки кода используйте тег , для нескольких строк кода — тег , если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
- Если что-то непонятно в статье — пишите, что именно и с какого места.