09. Функции. Часть 3. Конструкторы
Что такое прототип
![]() |
Cоздание класса в JavaScript выполняется описанием функции-конструктора. А создание объекта — экземпляра класса — при помощи вызова конструктора в качестве операнда операции new:
|
При этом в создаваемый объект копируются все свойства, описанный в конструкторе при помощи ключевого слова this. Именно все свойства, в том числе и те, которые являются методами (вспомним, что свойство в JavaScript условно называется методом, если его значением является функция).
Когда в экземпляр класса из конструктора копируются свойства-данные — это правильно. Ведь в каждом экземпляре они могут иметь разные значения, как, например, размеры прямоугольников в объектах rect1 и rect2:
var rect1 = new Rectangle(1,2); // Создали первый экземпляр
var rect2 = new Rectangle(10,20); // Создали второй экземпляр
Но в каждый объект из конструктора копируются ещё и методы. Значит, и в rect1, и в rect2 копируются функция square, вычисляющая площадь прямоугольника.
Это явно лишнее. Если данные могут быть разными, и могут меняться во время работы с объектами, то функции одни и те же.
В языках программирования, основанных на настоящих классах, в объект копируются только данные, а функции остаются в классе для общего пользования.
А как же псевдоклассы JavaScript? Они такие нерациональные?
Дело в том, что функции в JavaScript — это обычные данные, их можно менять, а значит, иногда есть смысл передавать методы конструктора в создаваемые с его помощью объекты. JavaScript это допускает, и в этом проявляется мощь, гибкость (и сложность, конечно) этого языка.
Но чаще всего методы, описанные в классе, неизменны, поэтому есть смысл хранить их только в конструкторе в единственном экземпляре, а не копировать в создаваемые объекты.
И это, действительно, можно (и нужно делать) при помощи прототипа.
Все функции (как экземпляры класса Function) имеют свойство prototype (прототип). Начальным значением этого свойства является объект с единственным свойством constructor, которое содержит ссылку на саму функцию.
Пусть определена функция f:
function f(x)
{
return x*x;
}
JavaScript для каждой функции создает свойство prototype (ведь функция — это объект, значит, у неё могут быть свойства). Для функции f создаётся свойство f.prototype. Свойство f.prototype содержит ссылку на объект, содержащий единственное свойство constructor.
Таким образом, f.prototype.constructor — то же самое, что f:
function f(x) { return x*x; }
alert(f.prototype.constructor(2)); // 4
А теперь раскроем ещё одну особенность работы оператора new. Мы уже говорили, что оператор new создаёт пустой объект, а затем вызывает функцию-конструктор. Но это не все. После создания пустого объекта оператор new устанавливает в этом объекте ссылку на свойство prototype функции-конструктора. Любые свойства, добавленные к прототипу конструктора, автоматически становятся «как бы» свойствами объектов, создаваемых с его помощью. «Как бы» сказано потому, что эти свойства не копируются в объекты, а хранятся в прототипе конструктора в единственном экземпляре.
Это именно то, что нужно! Теперь мы должны принять на вооружение следующее правило.
Свойства нужно описывать в конструкторе. Методы нужно описывать в прототипе конструктора.
Сделаем это на примере построения класса Rectangle.
// Конструктор класса Rectangle
function Rectangle(width, height)
{
// Свойства класса
this.width = width;
this.height = height;
}
// Методы класса
Rectangle.prototype.square=function(){return this.width*this.height;};
С объектами можно работать, как и прежде:
var rect1 = new Rectangle(1,2); // Создали первый экземпляр
var rect2 = new Rectangle(10,20); // Создали второй экземпляр
rect1.square(); // 2 -- нашли площадь первого прямоугольника
rect2.square(); // 200 -- нашли площадь второго прямоугольника
Мы действуем так, будто метод square принадлежит каждому экземпляру класса Rectangle. И цикл for/in подтвердит это:
function alertob(ob)
{
var str = "{";
for(p in ob) str += p+":"+ob[p]+",";
str = str.substring(0, str.length-1) + "}";
alert(str);
}
var rect = new Rectangle(12,85);
alertob(rect); // {width:12,height:85,
// square:function(){return this.width*this.height;}}
Но метода square теперь реально в объекте нет, он лежит в прототипе конструктора в единственном экземпляре, хотя доступен во всех объектах, созданным при помощи этого конструктора, словно свой собственный метод.
Смотрите пример «Конструктор класса Rectangle с прототипом».
Понятно, что прототип конструктора — идеальное место для хранения методов класса, а также констант класса.
Например, мы можем добавить в прототип конструктора Rectangle константу квадратный дюйм:
// Конструктор класса Rectangle
function Rectangle(width, height)
{
// Свойства класса
this.width = width;
this.height = height;
}
// Методы и константы класса
Rectangle.prototype.square=function(){return this.width*this.height;};
Rectangle.prototype.IN2=2.54*2.54; // Добавили константу в прототип
// Создать прямоугольник (размеры в сантиметрах)
var rect = new Rectangle(12,85);
// Вычислить его площадь в квадратных дюймах
alert(rect.IN2*rect.square()); // 6580.632
Смотрите пример «Константы класса».