Original article.
Когда я только начинал программировать на Javascript я создавал мои переменные и инкапсулировал функциональность, которая мне нужна, в функцию. Так как мои задачи становились более сложными и я начал изучать OOP в других языках, я увидел преимущества использования OOP в Javascript.
Чем больше становились скрипты, тем больше связей возникало между функциями. Предположим, что у вас есть десять функций на странице, шесть из которых вызывают одна другую для выполнения задачи, а другие четыре делают что-то совершенно другое. Тот, кто впервые увидит этот код, несомненно не получит четкого пердставления о структуре скрипта. Этот как раз тот случай, когда нужен объект.
В настоящем объектно-ориентированном смысле, объект представляет "a thing". Это может быть изображение, пользователь, документ - все, что угодно, что представлено именем существительным. Объект содержит свойства, которые его описывают и методы, которые описывают действия этого объекта (или что можно сделать с объектом). Например, array в javascript - это объект. У него есть свойства, такие как length, которые хранят информацию об объекте, и методы, такие как push, которые что-то делают с объектом.
Главная идея здесь простая - objects encapsulate related functionality.
В Javascript-е мы часто выполняем серии задач на существующих объектах. Например, валидация формы: "Когда эта форма сабмитится, проверить что пользователь вввел валидный e-mail, телефон и т.д." Вместо создания функции для проверки каждой формы на странице, почему не собрать связанные задачи в объект FormValidator(tm) , для того чтобы можно было легко использовать его на других страницах и других сайтах? Даже задачи, по типу установки onclick события на коллекцию ссылок лучше вынести в объект.
Мы установили, что объекты хороши в использовании, но что надо сделать, чтобы создать их? Javascript очень гибкий язык, предлагающий два основных метода для создания объектов, каждый со множеством смыслов и вариаций. Вот они, два метода, о которых мы говорим:
- using the object literal
- using the
new keyword with a function
Давайте прольем немного света на эти варианты и опишем преимущества и недостатки каждого.
Using the Object Literal
Создание объекта с использованием объектного литерала очень простое. Объектный литерал заключается в фигурные скобки с нулем и больше пар "свойство:значение", разделенных запятой. Каждое свойство и его значение разделяется двоеточием. Именем свойства может быть идентификатор, строка или число. Значением свойства может быть строка, число, функция или другой объект. Имя свойства конвертируется в строку, таким образом строка "25" и число 25 - одно и то же.
Пример:
{
property: value,
property: value
}
Создание объектов:
Для демонстранстрации, давайте создадим новый объект, с тремя свойствами, два хранят числовые значения,третье - это анонимная функция( функция без имени )
var AnimationManager =
{
framesPerSecond: 30,
totalLength: 15,
startAnimation: function(){ /* code goes here */ }
}
Важно понять, что объектный литерал - это просто shortcut для создания объекта, используя встроенный Object-type. Мы изучим этот код более детально позже, а сейчас просто запомним, что мы также можем создать
AnimationManager объект следующим образом:
var AnimationManager = new Object();
AnimationManager.framesPerSecond = 30;
AnimationManager.totalLength = 15;
AnimationManager.startAnimation = function () { /* code goes here */ };
Доступ к свойствам объекта
Мы назначили объектный литерал переменной
AnimationManager и теперь можем получить доступ к свойствам объекта используя точечную нотацию или скобки. Следующие строки идентичны:
alert(AnimationManager.framesPerSecond); // object.property
alert(AnimationManager['framesPerSecond']); // object['property']
В основном мы будем использовать точечную нотацию, но возможность обращаться к объекту, как к hash-массиву - очень удобная возможность. Например, мы можем написать функцию, которая будет устанавливать значения для полей
framesPerSecond or
totalLength:
function changeValue(property, value)
{
if (property == "framesPerSecond")
AnimationManager.framesPerSecond = value;
else
AnimationManager.totalLength = value;
}
Или можно использовать возможность доступа к свойствам через скобки:
function changeValue(property, value)
{
AnimationManager[property] = value;
}
Таким образом мы можем упростить код - одна строка вместо серии if-else
Добавление свойств или методов
Нашему объекту, который теперь связан с переменной, мы можем добавить новые свойства или методы в любое время. Просто создайте новое свойство, используя точечную нотацию или скобки, как если бы вы получали доступ к существующему свойству:
AnimationManager.stopAnimation = function(){ }
AnimationManager.defaultTween = "sinoidal";
You’ll recall that is exactly what we did earlier when we discussed the alternate new Object() syntax for the object literal.
Использование Function
В javascript функция - это объект. Создавая функцию - вы создаете объект. Объекты функций более полезны, чем объектные литералы, потому что они могут быть использованы как шаблоны для новых объектов.
Для тех, кто незнаком с OOP, этот объектный шаблон более известен, как класс. В отличие от объектов, которые каждый раз строятся заново, объект, созданный на основе класса - уже заполнен методами и свойствами. Определяя new function object, мы в результате определяем шаблон (blueprint) для новых объектов, которые мы можем использовать снова и снова.
Например, мы создали
AnimationManager в передыдущем разделе, используя объектный литерал. Нам действительно нужен только один менеджер, таким образом у нас нет необходимости предоставлять второй механизм. Объектный литерал - отличный выбор для этого объекта. Давайте немного расширим пример и представим, что
AnimationManager контролирует множеством объектов
Animation. Если мы объявим
Animation, используя объектный литерал, нам придется писать этот код еще и еще для каждого анимированного объекта. Создание таких объектов на базе шаблона предоставляет нам решение этой проблемы.
Использование шаблона
Чтобы использовать наш шаблон для создания объекта, мы используем ключевое слово
new и вызов шаблона функции, передавая в нее аргументы:
function Animation(element)
{
this.animationLength = 30;
this.element = element;
}
var obj = document.getElementById('login');
var animateLogin = new Animation(obj);
Во время выполнения
new Animation(obj) создается пустой объект и вызывается
Animation, связывая новый объект с его
this ключевым словом. Мы можем связать свойства объекта с его
this keyword и как результат выполнения функции вернется новый объект со всеми его свойствами. Таким образом новый объект будет имеет два свойства
animationLength и
element и связан с переменной
animateLogin для дальнейшего использования.
Мы также можем создать методы в нашем шаблоне, просто добавляя функции:
function Animation(element)
{
this.animationLength = 30;
this.element = element;
this.onStart = function ()
{
alert("The animation is starting!");
};
this.onEnd = function ()
{
alert("The animation is ending!");
};
}
var obj = document.getElementById('login');
var animateLogin = new Animation(obj);
Важно понять, что
мы можем в
Animation функцию добавлять свойства как объектные литералы:
function Animate() { }
Animate.animationLength = 30;
Animate.element = element;
Разница здесь в том, что эти дополнительные свойства не являются частью шаблонаЕсли мы выполним
new Animation() из кода выше, мы получим назад пустой объект - не объект с двумя свойствами. Например:
var animateLogin = new Animation(loginform);
Animation.animationLength = 30;
alert(animateLogin.element); // the 'loginform' element
alert(animateLogin.animationLength); // undefined!
Prototype
Метод описанный выше работает замечательно, но он немного непроизводительный. В частности, создание тысячи объектов
Animation создаст тысячу копий каждого метода и свойства, занимая ценную память, которую мы могли бы использовать для других вещей ( и создавая возможную
утечку памяти). К счастью для нас, есть путь создания шаблонного объекта, который генерит объекты, как ссылки на те же самые методы и свойства: Мы назначаем свойства шаблону через
prototype. Поступая таким образом, все объекты, инициализированные из оргинального шаблона могут использовать методы и свойства из прототипа шаблона вместо создания их собственных копий. Короче говоря, вызов свойства объекта в первую очередь проверяет объект на наличие этого свойства, если оно не существует, проверяется прототип шаблона. Таким образом существует только одна версия свойства, сохраненная в памяти, невзирая на то, сколько объектов мы создали.
Синтаксис выглядит почти так же, как добавление свойства для объекта
Animation. Вместо
Animation.onStart мы добавляем свойство в функцию через свойство объекта prototype:
Animation.prototype.onStart. Давайте перепишем
Animation объект, чтобы быть увереными, что метода ссылаются, а не копируются:
function Animation(element)
{
this.animationLength = 30;
this.element = element;
}
Animation.prototype.onStart = function()
{
alert("The animation is beginning!");
};
Animation.prototype.onEnd = function()
{
alert("The animation is ending!");
};
var animateLogin = new Animate(loginform);
animateLogin.onStart();
Мы можем добавить в прототип шаблона в любое время и новые свойства будут автоматически доступны, даже если объект был уже создан. Давайте перестроим последний пример для демонстрации:
function Animation(element)
{
this.animationLength = 30;
this.element = element;
}
var animateLogin = new Animate(loginform);
Animation.prototype.onStart = function()
{
alert("The animation is beginning!");
};
Animation.prototype.onEnd = function()
{
alert("The animation is ending!");
};
animateLogin.onStart();
Даже если мы создали новый
Animation объект
перед тем, как объявили
onStart и
onEnd методы, мы можем использовать их! Это огромное преимущество способа, сонованного на прототипе.
Объект, как результат выполнения конструктора
Как вы знаете, функции могут возвращать значения. Что интересно, если вы возвращаете объект, как результат выполнения конструктора (функция вызванная сключевым словом
new), этот объект будет использован как новый объект вместо пустого только что созданного объекта и назначенного слову
this. Любые свойства, которые вы определите В или ВНЕ функции будут недоступны. Пример объяснит это лучше:
function Animation(element)
{
this.animationLength = 30;
return {hello: "Hello, world!"}; // empty object.
}
Animation.prototype.onStart = function() { };
Animation.prototype.onEnd = function() { };
var animateLogin = new Animation(loginform);
animateLogin.onStart(); // undefined!
animateLogin.animationLength; // undefined!
alert(animateLogin.hello); // "Hello, world!"
"В чем фишка", спросите вы? Это позволяет нам создавать
приватные переменные, которые доступны внутри нашего объекта, но не доступны снаружи, как только что созданные. Давайте посмотрим на пример с использованием ключевого слова
new в функции:
function Animation(element)
{
var looped = 0;
var e = element;
function construct()
{
this.loopCount = function()
{
return looped;
}
this.loop = function()
{
looped++;
}
}
return new construct();
}
var animateLogin = new Animation();
animateLogin.loop();
alert(animateLogin.loopCount()); // it's 1
alert(animateLogin.looped); // undefined!
We created a new function called construct within our
Animation function. Anytime we create a new
Animation object, we end up returning a new construct object instead. Это означает, что методы
loopCount и
loop будут единственными, к кому мы будем иметь доступ. Тем не менее, благодаря
замыканиям переменная
looped все еще доступна внутри тех функций.
Singletons with functions
Сушествуют ситуации, когда у нас есть центральный объект и нам не нужно предоставлять механизм для создания его новых версий. The
singleton design pattern решает эту проблему. В примере, который мы используем,
AnimationManager - центральный объект, который должен контролировать когда анимация начинается и заканчивается. We only need one of these, as opposed to the individual animations that require separate Animation objects with unique values. Мы создали
AnimationManager с помощью объектного литерала, но вы будете часто встречать singletons implemented with functions. Давайте посмотрим, как это делается:
var AnimationManager = new function()
{
this.framesPerSecond = 30;
this.startAnimation = function(){ /* code goes here*/ }
}
Мы просто вызываем
new на анонимной функции и используем
this для установки свойств и методов. Создание нового объекта этим путем позволяет избежать потенциального использования объекта, как шаблона. Вуаля, это функция-одиночка! Если мы хотим быть очень осторожными в доступе к свойствам объекта, мы можем даже создать приватные переменные для нашего синглтона, используя ту же технику, что и в предыдущем разделе:
var AnimationManager = new function()
{
var framesPerSecond = 30;
function construct()
{
this.startAnimation = function() {}
this.getFPS = function() { return framesPerSecond; }
this.setFPS = function(fps) { framesPerSecond = fps; }
}
return new construct();
}
Преимущества этой техники над объектным литералом в том, что появляется возможность выполнять задачи в момент создания экземплята синглотона. Ни одна функция не может быть выполнена в момент создания экземпляра, когда мы используем объектный литерал. Для этого требуется дополнительный шаг для создания функции инициализации и запуска ее сразу после создания.
The Object Factory
Другой продвинутый паттерн, который вы можете видеть время от времени - это
factory паттерн. Не вдаваясь в подробности, мы можем использовать функцию для создания объектов, которые удобны для обработки объектных свойств. Давайте взглянем на пример:
function objectMaker()
{
return {value:5}
}
var object1 = objectMaker();
var object2 = objectMaker();
Это удобно потому что мы можем выборочно перекрывать значения, распределяя значения одного литерала в другой [by mapping the values of one literal onto another.]
The Prototype JavaScript Framework (не путайте со свойством prototype) поступает именно таким образом со своим Object.extend методом.