Говорят, что объект, созданный при помощи new
, является экземпляром конструктора.
Вот простой конструктор кроликов. Имена конструкторов принято начинать с заглавной буквы, чтобы отличать их от других функций.
function Rabbit(type) {
this.type = type;
}
var killerRabbit = new Rabbit("убийственный");
var blackRabbit = new Rabbit("чёрный");
console.log(blackRabbit.type);
// → чёрный
Конструкторы (а вообще-то, и все функции) автоматически получают свойство под именем prototype
, которое по умолчанию содержит простой пустой объект, происходящий от Object.prototype
. Каждый экземпляр, созданный этим конструктором, будет иметь этот объект в качестве прототипа. Поэтому, чтобы добавить кроликам, созданным конструктором Rabbit
, метод speak
, мы просто можем сделать так:
Rabbit.prototype.speak = function(line) {
console.log("А " + this.type + " кролик говорит '" + line + "'");
};
blackRabbit.speak("Всем капец...");
// → А чёрный кролик говорит 'Всем капец...'
Важно отметить разницу между тем, как прототип связан с конструктором (через свойство prototype
) и тем, как у объектов есть прототип (который можно получить через Object.getPrototypeOf
). На самом деле прототип конструктора – Function.prototype
, поскольку конструкторы – это функции. Его свойство prototype
будет прототипом экземпляров, созданных им, но не его прототипом.
Перегрузка унаследованных свойств
Когда вы добавляете свойство объекту, есть оно в прототипе или нет, оно добавляется непосредственно к самому объекту. Теперь это его свойство. Если в прототипе есть одноимённое свойство, оно больше не влияет на объект. Сам прототип не меняется.
Rabbit.prototype.teeth = "мелкие";
console.log(killerRabbit.teeth);
// → мелкие
killerRabbit.teeth = "длинные, острые и окровавленные";
console.log(killerRabbit.teeth);
// → длинные, острые и окровавленные
console.log(blackRabbit.teeth);
// → мелкие
console.log(Rabbit.prototype.teeth);
// → мелкие
На диаграмме нарисована ситуация после прогона кода. Прототипы Rabbit
и Object
находятся за killerRabbit
на манер фона, и у них можно запрашивать свойства, которых нет у самого объекта.
Перегрузка свойств, существующих в прототипе, часто приносит пользу. Пример с зубами кролика показывает, как её можно использовать для выражения каких-то исключительных характеристик конкретных экземпляров объектов, оставляя прочим стандартные значения из прототипа.
Та же перегрузка используется, чтобы дать стандартным функциям и массивам свои методы toString
, отличные от метода базового объекта.
console.log(Array.prototype.toString == Object.prototype.toString);
// → false
console.log([1, 2].toString());
// → 1,2
Вызов toString
массива выводит результат, похожий на .join(",")
– получается список, разделённый запятыми. Вызов Object.prototype.toString
напрямую для массива приводит к другому результату. Эта функция не знает ничего о массивах:
console.log(Object.prototype.toString.call([1, 2]));
// → [object Array]
Нежелательное взаимодействие прототипов
Прототип помогает в любое время добавлять новые свойства и методы всем объектам, которые основаны на нём. К примеру, нашим кроликам может понадобиться танец.
Rabbit.prototype.dance = function() {
console.log("А " + this.type + " кролик танцует джигу.");
};
killerRabbit.dance();
// → А убийственный кролик танцует джигу.
Это удобно. Но в некоторых случаях это приводит к проблемам. В предыдущих главах мы использовали объект как способ связать значения с именами – мы создавали свойства для этих имён, и давали им соответствующие значения. Вот пример из 4-й главы:
var map = {};
function storePhi(event, phi) {
map[event] = phi;
}
storePhi("пицца", 0.069);
storePhi("тронул дерево", -0.081);
Мы можем перебрать все значения фи в объекте через цикл for
/in
, и проверить наличие в нём имени через оператор in
. К сожалению, нам мешается прототип объекта.
Object.prototype.nonsense = "ку";
for (var name in map)
console.log(name);
// → пицца