Объектно-ориентированный JavaScript
Jonathan Snook, 15 сентября 2007
Когда я только начинал программировать на JavaScript, я старался всю пригодную к повторному использованию функциональность инкапсулировать в функции. Но по мере усложнения задач и изучения мной объектно-ориентированного программирования в других языках, мне захотелось применять в JavaScript что-то подобное.
В объектно-ориентированном программировании объект воспринимается как предмет, это может быть изображение, пользователь, документ, все, что можно описать существительным. У объекта есть свойства и методы, первые тоже можно описать существительными, они отражают состояние объекта, вторые можно описать глаголами они отражают действия, которые может выполнять объект. К примеру, array в JavaScript это объект, он содержит свойства, такие как length, которые содержат информацию об объекте и методы, такие как push, которые позволяют выполнять с объектом какие-то действия.
Основная идея заключается в том, что объекты инкапсулируют всю связанную с ними функциональность.
В JavaScript мы часто выполняем похожие задачи. В качестве примера можно взять, валидацию форм, когда пользователь подтверждает форму, нужно проверить, все ли обязательные поля заполнены, соответствуют ли стандартам адреса электронной почты и номера телефонов и, возможно, некоторые специфические поля. Почему бы, вместо создания нескольких функций не объединить, эту функциональность в одном объекте FormValidator, который можно легко использовать и на других сайтах. В дополнение ко всему, объекты позволяют уменьшить загрязнение глобального пространства имен, тем самым сделать код чище.
JavaScript очень гибкий язык, он предоставляет два основных метода создания объектов:
- использование объектного литерала,
- использование ключевого слова
new.
У каждого из них есть свои особенности, преимущества и недостатки, которые я попытаюсь раскрыть в этой статье.
Использование объектного литерала
Создание объекта с помощью объектного литерала, достаточно очевидно. Объектный литерал представляет из себя, заключенные в фигурные скобки, пары свойств и их значений, разделенные запятыми. В свою очередь каждое свойство отделено от его значения с помощью двоеточия. Имена свойств могут быть идентификаторами, строками или числами, а значения могут быть строками, числами, функциями или другими объектами. Имена свойств преобразуются в строки, это значит что строка "25" и число 25, указывают на одно и тоже свойство, и при использовании в одном объекте будут переназначать друг друга.
Пример:
{
property: value,
property: value
}
Создание объектов
Для демонстрации этого метода, давайте создадим простой объект с тремя свойствами, два из них будут хранить числа, тогда как третье будет анонимной функцией (функцией без имени):
var AnimationManager = {
framesPerSecond: 30,
totalLength: 15,
startAnimation: function() { /* code */ }
}
Стоит обратить внимание на то, что объектный литерал, это всего лишь сокращенный способ создания объектов с помощью встроенного типа Object, а вышеприведенный пример можно переписать так:
var AnimationManager = new Object();
AnimationManager.framesPerSecond = 30;
AnimationManager.totalLength = 15;
AnimationManager.startAnimation = function () { /* code */ };
Доступ к свойствам объекта
Мы создали объект AnimationManager и теперь можем получить доступ к его свойствам с помощью точки или квадратных скобок, следующие две строки кода дают один и тот же результат:
alert(AnimationManager.framesPerSecond); // object.property
alert(AnimationManager['framesPerSecond']); // object['property']
Чаще для доступа к свойствам мы будем использовать точку, но возможность доступа к объекту как к хеш таблице может быть полезна, если нужен гибкий доступ к нескольким похожим свойствам. К примеру, нам может понадобиться функция, устанавливающая свойства framesPerSecond и totalLength, используя только точку, мы будем вынуждены обратиться к такому коду:
function changeValue(property, value) {
if (property == "framesPerSecond") {
AnimationManager.framesPerSecond = value;
} else {
AnimationManager.totalLength = value;
}
}
А, зная о том, что к свойствам можно обращаться с помощью квадратных скобок, мы получим более компактный код:
function changeValue(property, value) {
AnimationManager[property] = value;
}
Добавляем свойства или методы
В процессе работы с объектом мы можем, создать новые свойства или методы в любой момент. Создать новое свойство и присвоить ему значение, так же просто, как и присвоить значение существующему:
AnimationManager.stopAnimation = function() { }
AnimationManager.defaultTween = "sinoidal";
Использование функций
В JavaScript функция является объектом и может быть использована в качестве шаблона для новых объектов.
В предыдущей главе, создавая AnimationManager, нам не нужно было заботиться о механизме создания нескольких таких объектов, поэтому объектный литерал был отличным выбором. Используя функцию объект, мы получаем возможность создать своеобразный чертеж объекта, по которому можно будет воспроизводить его снова и снова.
Давайте расширим предыдущий пример с AnimationManager, и дадим ему возможность контролировать множество объектов Animation. Если бы мы решили определить 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, что позволяет нам создать необходимые свойства этого объекта. После выполнения строки var animateLogin = new Animation(obj); переменная animateLogin будет ссылаться на объект Animation, а его свойствам animationLength и element будут присвоены соответствующие значения.
Мы можем создать и методы, просто присваивая свойствам объекта функции:
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 Animation() { }
Animation.animationLength = 30;
Animation.element = element;
Разница в том, что в таком случае они не будут частью шаблона. Если мы выполним new Animation() после приведенного выше кода, то получим пустой объект, вместо объекта с двумя свойствами:
var animateLogin = new Animation(loginform);
Animation.animationLength = 30;
alert(animateLogin.element);
alert(animateLogin.animationLength); // undefined!
Прототипы
Приведенный подход работает замечательно, но он немного расточителен. В случае создания тысячи объектов Animation будет создана тысяча копий каждого метода и свойства, что потребует много памяти и может привести к ее потерям. К счастью, существует способ создания шаблонов, по которым можно создать объекты, использующие общие свойства и методы. Для этого нам нужно присоединить свойства к prototype шаблона, после чего все объекты созданные на основе этого шаблона будут использовать свойства и методы прототипа вместо создания собственных. При обращении к свойству или методу объекта, сначала проверяются его наличие у объекта, а если у объекта соответствующего свойства нет, то используется свойство прототипа.
Синтаксис очень похож на простое добавление свойств объекту, но вместо самого объекта мы добавляем их к его свойству prototype:
function Animation(element) {
this.animationLength = 30;
Animation.element = element;
}
Animation.prototype.onStart = function() {
alert("The animation is beginning!");
};
Animation.prototype.onEnd = function() {
alert("The animation is ending!");
};
var animateLogin = new Animation(loginform);
animateLogin.onStart();
В любой момент мы можем добавить новые свойства к прототипу шаблона, и они будут доступны для всех созданных с его помощью объектов:
function Animation(element){
this.animationLength = 30;
Animation.element= element;
}
var animateLogin = new Animation(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) то он будет использован как новый объект, вместо пустого объекта созданного new и связанного с this. Свойства, которые определены в функции или за ее пределами не будут доступны:
function Animation(element) {
this.animationLength = 30;
return {
hello:"Hello, world!"
};
}
Animation.prototype.onStart = function() { };
Animation.prototype.onEnd = function() { };
var animateLogin = new Animation(loginform);
animateLogin.onStart(); // undefined!
animateLogin.animationLength; // undefined!
alert(animateLogin.hello); // "Hello, world!"
Это позволяет нам создать приватные переменные, которые доступны только внутри нашего объекта:
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!
Мы создали новую функцию construct в функции Animation, и каждый раз создавая новый объект Animation, мы на самом деле создаем объект construct, это значит, что у нас есть доступ только к методам loopCount и loop, тем не менее, благодаря закрытию, переменная looped остается доступной внутри этих функций. Таким образом, мы можем скрыть механизм работы счетчика и гарантировать, что доступ будет осуществляться только по установленным нами правилам.
Создание синглтона с помощью функции
Бывают ситуации, когда мы хотим, чтобы объект был единственным, и не было возможности создания других его экземпляров. Решить эту проблему позволяет паттерн синглтон. В примере, который мы используем на протяжении всей статьи, AnimationManager центральный объект, контролирующий начало и завершение анимации, нам нужен только один экземпляр этого объекта, поэтому он был создан с помощью объектного литерала, но часто можно видеть и реализацию синглтона с помощью функции:
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();
}
Преимущество этой техники перед объектным литералом в том, что после создания синглтона можно выполнить его инициализацию, в случае с объектным литералом для этого потребовалось бы создать дополнительную функцию и вызвать ее сразу после создания объекта.
Фабрика объектов
Другой полезный паттерн, который время от времени приходится использовать, это фабрика объектов. Не вдаваясь в детали, приведу пример его реализации на JavaScript.
function objectMaker() {
return {value:5}
}
var object1 = objectMaker();
var object2 = objectMaker();
Итоги
В последнее время JavaScript переживает настоящее возрождение, с его помощью создаются все более сложные приложения, в которых объектно-ориентированный подход может значительно упростить восприятие кода, его поддержку и повторное использование.
Ресурсы
- Dean Edwards’ Base базовые классы и наследование в JavaScript.
- Closures
- Prototype Library
- Prototypal Inheritance
- Private Members in JavaScript

чтобы при создании или присваивании свойств объекта с помощью "new" много не писать, можно использовать оператор "with":
with(obj){
property1 = value1
property2 = value2
}
, with не рекомендуется к использованию...
Javascript - er, мдя, ну ладно ))
Я всё равно пользуюсь объектными литералами.
Вот бы еще наследование было... (может есть?)
Имел ввиду "нормальное" :) а не var Child = new Parent();
Ой, ошибся - Child.prototype = new Parent(); [nuclon.habrahabr.ru (с)]
Нашел статью "Наследование в JavaScript" - http://covex.habrahabr.ru/blog/19515.html
Существует ещ' один подход к ООП в JS, которій используется, например, в библиотеке AJAX.OOP - http://ajaxoop.org/
Отличная статья! Мне очень помогла начать использовать ООП в JS.
Статья показалась мне настолько хорошей что поставил на не ссылку в википедии.
Удачи!
Спасибо огромное. очень помогло, пока нигде не нашёл более толкового объяснения. Респект автору