Как можно видеть, объекты в JavaScript являются чрезвычайно полезными. К сожалению, часто приходится объявлять слишком много информации каждый раз, когда мы хотим создать объект. Например, давайте создадим объект Animal:
var myAnimal = {
name: 'felix',
species: 'cat',
talk: function(){ alert('Meow!'); },
callOver: function(){ alert(this.name+' ignores you'); },
pet: function(){ alert('Purr!'); }
}
Мы имеем переменную myAnimal, которая представляет кота (cat) с именем Феликс (felix). Однако, если потребуется создать другого кота, нам понадобиться ввести все это снова. Здесь на помощь приходит объектно-ориентированный подход. Вместо перепечатки всякий раз всего объекта можно определить функцию, которая создает для нас аналогичный объект:
function Cat(name){
this.name = name;
this.species = 'Cat';
this.talk = function(){ alert('Meow!'); }
this.callOver = function(){ alert(this.name+' ignores you'); },
this.pet = function(){ alert('Purr!'); }
}
var felix = new Cat('Felix');
var sam = new Cat('Sam');
var patty = new Cat('Patty');
felix.pet(); // выводит 'Purr!'
sam.callOver(); // выводит 'Sam ignores you'. Просто, как кот!
alert(patty.species); // выводит 'Cat'
В этом примере создана одна функция Cat, а затем создаются три новых кота с помощью этой функции: Felix, Sam и Patty. Каждый из этих котов имеет одинаковые функции: talk, callOver и pet, и каждая из них имеет свойство species, заданное как 'cat'. И подобно всем котам, они в равной степени своенравны.
Обычно говорят, что felix, sam и patty являются экземплярами одного объекта: Cat. Код самой функции Cat называется конструктором. Мы передаем ей переменную name и используем ее для задания this.name. К сожалению, при объявлении каждой функции Cat в конструкторе в действительности создается новая копия каждой функции всякий раз при создании нового Cat. Так как функции talk, callOver и pet являются идентичными, то нам в действительности требуется только одна копия каждой функции. Здесь на помощь приходит прототипирование.
Можно переписать функцию Cat таким образом, чтобы каждая функция объявлялась только один раз:
function Cat(name){
this.name = name;
}
Cat.prototype.species = 'Cat';
Cat.prototype.talk = function(){ alert('Meow!'); };
Cat.prototype.callOver = function(){ alert(this.name+' ignores you'); };
Cat.prototype.pet = function(){ alert('Purr!'); };
Синтаксис этого метода немного отличается от предыдущего. Вместо объявления всех свойств и методов внутри функции Cat, они теперь объявляются с помощью Cat.prototype. Это может показаться более сложным, но предлагает много преимуществ. Предположим, например, что надо добавить новую функцию sleep для каждого имеющегося cat. Это можно сделать двумя способами. Первый: можно добавить функцию sleep в felix, sam и patty. Это, однако, не только трудоемко, но также и неэффективно. Если бы имелось 500 cat, то потребовалось бы сначала отследить всех этих 500 котов, а затем добавить функцию каждому cat.
Однако с помощью прототипов можно добавить функцию sleep всем cat одновременно:
Cat.prototype.sleep = function(){ alert(this.name+' falls asleep'); };
Этот способ не только быстрее, но к тому же нам больше не требуется отслеживать каждого cat, чтобы добавить функцию sleep.
Существует более быстрый способ добавления прототипов. С помощью объектного литерала можно одновременно задать любое количество прототипов свойств или методов.
function Cat(name){
this.name = name;
}
Cat.prototype = {
species: 'Cat',
talk: function(){ alert('Meow!'); },
callOver: function(){ alert(this.name+' ignores you'); },
pet: function(){ alert('Pet!'); }
}
Важно отметить, что с помощью этого метода можно задать свойства функции только один раз. После этого необходимо использовать предыдущий метод.
Object.prototype.method = function(){ ... }.
Если попробовать теперь добавить в cat метод sleep с помощью этого метода:
Cat.prototype = {
sleep: function(){ alert(this.name+' falls asleep'); }
}
то предыдущие прототипы, species, talk, callOver и pet, будут все удалены. Единственным имеющимся прототипом для cat будет sleep.
Прототипы можно также использовать для расширения встроенных объектов JavaScript. Можно реализовать, например, функцию String.prototype.reverse, которая будет возвращать любую созданную строку в обратном порядке:
String.prototype.reverse = function(){
var out = '';
for(var i=this.length-1; i>=0; i--){
out+=this.substr(i, 1);
}
return out;
}
alert('asdf'.reverse());
Это может быть очень полезным инструментом при правильном использовании. Можно реализовать String.prototype.trim() для удаления из строки всех пробелов, Date.prototype.dayName - для получения названия дня недели из объекта Date и т.д. Однако настоятельно рекомендуется воздержаться от добавления каких-либо прототипов в Array или Object, так как в этом случае циклы for-in для двух этих типов данных будут работать неправильно. Рассмотрим следующий пример:
var myArray = [1, 2, 3];
for(n in myArray) alert(n); // выводит 0, 1 и 2 - индексы массива.
Array.prototype.something = function(){ };
for(n in myArray) alert(n); // выводит 'something', 0, 1 и 2.
Как можно видеть, здесь выполнено прототипирование Array и добавлена функция something. Однако теперь эта функция something видна как элемент массива, результат, который определенно не ожидался и не требовался. То же самое происходит с объектами и объектными литералами, если выполнить прототипирование Object. Если можно быть абсолютно уверенным, что цикл for-in никогда не будет использоваться и никакой другой разработчик не будет использовать этот код JavaScript, то можно применять прототипирование для Array или Object, но надо помнить о связанных с этим проблемах. Однако существуют другие методы для достижения тех же результатов. Например, для расширения Array можно задействовать следующий метод без прототипирования: