Мы видели, насколько неэффективным может быть применение конструктора для присваивания методов инициализируемому объекту. При этом каждый созданный конструктором объект имеет идентичные копии одних и тех же свойств-методов. Есть значительно более эффективный способ задания методов, констант и других свойств, общих для всех объектов класса.
Объекты JavaScript «наследуют» свойства от объекта-прототипа. Каждый объект имеет прототип; все свойства объекта-прототипа становятсясвойствами любых объектов, для которых он является прототипом. То есть любой объект наследует свойства от своего прототипа.
Прототип объекта определяется функцией-конструктором, используемой для создания и инициализации объекта. Все функции в JavaScript имеют свойство prototype, ссылающееся на объект. Этот объект-прототип изначально пуст, однако любые свойства, которые вы определите в нем, будут наследоваться всеми объектами, созданными с помощью конструктора.
Конструктор определяет класс объектов и инициализирует свойства, такие как width и height, являющиеся для класса переменными состояния. Объектпрототип связан с конструктором, так что каждый член класса наследует тот набор свойств, который имеется в прототипе. Это значит, что объектпрототип – идеальное место для методов и других свойств-констант.
Обратите внимание, что наследование осуществляется автоматически, как часть процесса поиска значения свойства. Свойства не копируются из объекта-прототипа в новый объект; они просто присутствуют, как если бы они были свойствами этих объектов. Это имеет два важных последствия. Во-первых, использование объектов-прототипов может в значительной степени уменьшить объем памяти, требуемый для каждого объекта, так как объекты могут наследовать многие из своих свойств. Во-вторых, объект наследует свойства, даже если они были добавлены в его прототип после создания объекта. У каждого класса имеется один объект-прототип, с одним наборов свойств. Но потенциально может существовать много экземпляров класса, каждый из которых наследует свойства прототипа. Свойство прототипа может наследоваться многими объектами, поэтому JavaScript должен обеспечить фундаментальную асимметричность между чтением и записью значений свойств.
Когда вы читаете свойство p объекта o, JavaScript сначала проверяет, есть ли у объекта o свойство с именем p. Если такого свойства нет, то проверяется, есть ли свойство с именем p в объекте-прототипе. Так работает наследование на базе прототипов. С другой стороны, когда свойству присваивается значение, JavaScript не использует объект-прототип. Чтобы понять, почему это так, подумайте, что произошло бы в этом случае: предположим, вы пытаетесь установить значение свойства o.p, а у объекта o нет свойства с именем p. Предположим теперь, что JavaScript идет дальше и ищет свойство p в объекте-прототипе объекта o и позволяет вам изменить значение свойства прототипа. Но вы изменили значение p для всего класса объектов, а это вовсе не то, что вам требовалось. Поэтому наследование свойств происходит только при чтении значения свойств, но не при записи в них. Если вы устанавливаете свойство p в объекте o, который наследует это свойство от своего прототипа, происходит создание нового свойства непосредственно в объекте p. Теперь, когда объект o имеет собственное свойство с именем p, он больше не наследует значение p от прототипа. Когда вы читаете значение p, JavaScript сначала ищет его в свой-ствах объекта o. Так как он находит свойство p, определенное в o, ему не требуется искать его в объекте-прототипе, и JavaScript никогда не найдет значение p, определенное в нем. Мы иногда говорим, что свойство p «затеняет» или «скрывает» свойство p объекта-прототипа. Наследование прототипов может показаться запутанным.Свойства прототипа совместно используются всеми объектами класса, поэтому, как правило, их имеет смысл применять только для определения свойств, совпадающих для всех объектов класса. Это делает прототипы идеальными для определения методов. Другие свойства с постоянными значениями (такими как математические константы) также подходят для определения в качестве свойств прототипа. Если класс определяет свойство с очень часто используемым значением по умолчанию, то можно определить это свойство и его значение по умолчанию в объекте-прототипе. Тогда те немногие объекты, которые хотят изменить значение по умолчанию, могут создавать свои частные копии свойства и определять собственные значения, отличные от значения по умолчанию.
Перейдем от абстрактного обсуждения наследования прототипов к конкретному примеру. Предположим, что мы определили функцию-конструктор Circle() для создания объектов, представляющих круг. Объект-прототип для этого класса – Circle.prototype,1 поэтому мы можем определить константу, доступную всем объектам Circle:
Circle.prototype.pi = 3.14159;
Полноценное воплощение этой идеи приведено в примере 8.4. Код определяет класс Circle, сначала определяя конструктор Circle() для инициализации каждого отдельного объекта, а затем устанавливая свойства Circle.prototype для определения методов и констант, совместно используемых всеми экземплярами класса.
Пример 8.4. Определение класса Circle с помощью объекта-прототипа
// Определяем метод-конструктор для нашего класса.
// Используем его для инициализации свойств, различных для каждого
// отдельного объекта Circle.
function Circle(x, y, r)
{
this.x = x; // Координата X центра круга.
this.y = y; // Координата X центра круга.
this.r = r; // Радиус круга.
}
// Создаем и отбрасываем первоначальный объект Circle.
// Это приводит к созданию объекта-прототипа в JavaScript 1.1.
new Circle(0,0,0);
// Определяем константу: свойство, которое будет совместно использоваться
// всеми объектами circle. На самом деле нам следовало бы использовать// Math.PI, но для целей обучения мы поступаем именно так.
Circle.prototype.pi = 3.14159;
// Определяем метод для вычисления длины окружности.
// Сначала объявляем функцию, а затем присваиваем ее свойству прототипа.
// Обратите внимание на использование константы, определенной выше.
function Circle_circumference() { return 2 * this.pi * this.r; }
Circle.prototype.circumference = Circle_circumference;
// Определяем еще один метод. В этот раз мы одновременно воспользуемся
// функциональным литералом для определения функции и присвоим его
// свойству прототипа.
Circle.prototype.area = function() { return this.pi * this.r * this.r; }
// Класс Circle определен.
// Теперь мы можем создать экземпляр класса и вызывать его методы.
var c = new Circle(0.0, 0.0, 1.0);
var a = c.area();
var p = c.circumference();
Прототипы и встроенные классы
Не только классы, определенные пользователем, имеют объекты-прототипы. Встроенные классы, такие как String и Date, также имеют объекты-прототипы, и вы можете присваивать им значения.1 Например, следующий код определяет новый метод, доступный всем объектам String:
// Возвращает true, если последним символом является c
String.prototype.endsWith = function(c) {
return (c == this.charAt(this.length-1))
}
Определив новый метод endsWith() в объекте-прототипе String, мы сможем обратиться к нему следующим образом:
var message = "hello world";
message.endsWith('h') // Возвращает false
message.endsWith('d') // Возвращает true