Паттерны организации кода на чистом JavaScript, JQuery и Prototype
Обновление от 27.11.2011: Доступно обновление для этого сниппента :)
Гибкость языка JavaScript имеет обратную сторону — не существует единого способа создания т.н. reusable component'ов. В традиционных объектно-ориентированных языках подобные решения принимаются создателями языка, например, путём введения в язык неймспейсов, классов и интерфейсов.
Все скрипты ниже будут делать одно и то же — по клику на определённый элемент выводить
окно сообщения с id элемента, на котором был произведён клик.
Все примеры посроены по одному принципу: сначала создаётся компонент, пригодный для повтороного использования, после загрузки
HTML-документа создаётся один экземпляр такого компонента. В каждом из примеров уделено внимание традиционно сложному
для понимания моменту — контексту исполнения функций (вопрос «А чему равен this в этой функции?!» :)).
Тестовый HTML-документ очень прост, в теге <body> всего одна строка:
<a id="clickMe" href="http://google.com.ua">Кликни!</a>
Классика
Классикой является т.н. JavaScript Module Pattern.
Чистый JavaScript без использования фреймворков
Для полной реализации примера понадобилось дописать несколько кроссбраузерных функций, заодно пригодился один из способов реализации паттерна Singleton на JavaScript. Также применяется эмуляция namespace'ов с помощью вложенных объектов.
// Эмулируем namespace'ы
var CrispStudio = {JSTest:{}};
// Функции, для обеспечения минимальной кроссбраузерности,
// использован паттерн singleton
CrispStudio.JsUtils = {
$ : function(id)
{
if (document.getElementById != null)
{
return document.getElementById(id);
}
if (document.all != null)
{
return document.all[id];
}
},
addListener : function(objObject, strEventName, fnHandler)
{
if (objObject.addEventListener)
{
objObject.addEventListener(strEventName, fnHandler, false);
}
else
{
if (objObject.attachEvent)
{
objObject.attachEvent("on" + strEventName, fnHandler);
}
}
},
getEventSrc : function(e)
{
if (!e)
{
e = window.event;
}
if (e.originalTarget)
{
return e.originalTarget;
}
else
{
if (e.srcElement)
{
return e.srcElement;
}
}
},
preventDefault : function(event)
{
if (event.preventDefault)
{
event.preventDefault()
}
else
{
event.returnValue = false;
}
}
}
// Создание "класса"
CrispStudio.JSTest.Clicker = function(options)
{
// "private"-переменные
this.element = CrispStudio.JsUtils.$(options.elementId);
this.text = options.text;
// Вызов "конструктора"
this.initialize();
}
// Добавление методов
CrispStudio.JSTest.Clicker.prototype =
{
// "Конструктор",
// внутри этой функции this -- объект нашего "класса"
initialize : function()
{
// private-метод
this.showMessage = function(elementId)
{
alert(this.text + elementId);
}
// Добавляем обработчик
CrispStudio.JsUtils.addListener(this.element, 'click', this.doClick);
//ВАЖНО! Сохраняем ссылку на объект нашего "класса" в DOM-элементе
this.element.modelObj = this;
},
//внутри этой функции this -- элемент, на котором клинули
doClick : function(event)
{
var clickedElement = CrispStudio.JsUtils.getEventSrc(event);
// ВАЖНО! Получаем ссылку на объект нашего класса, сохранённую в DOM-элементе
var $this = clickedElement.modelObj;
// Вызов private-метода объекта класса
$this.showMessage(clickedElement.id);
// Отключаяем переход по ссылке
CrispStudio.JsUtils.preventDefault(event);
}
}
CrispStudio.JsUtils.addListener(window, 'load', function(){
// Создаём экземпляр "класса"
new CrispStudio.JSTest.Clicker({elementId: 'clickMe', text: 'Клик на элементе: '});
});
jQuery
Этот фреймворк использует собственный паттерн, называемый plugin, основанный на замыкании,
кроме этого очень интересно используется конструкция (function(){})():
jQuery.noConflict(); //обеспечиваем совместимость с другими библиотеками
//Определяем плагин JQuery
(function($) {
$.fn.extend({
clicker : function(options)
{
var defaults = {text: 'Default values'};
// Поддерживаем вызов методов в цепочку
return $(this).each(function(){
// Получаем настройки\параметры
var config = $.extend({}, defaults, options);
function showMessage(text) //private-метод
{
alert(text);
}
// this здесь -- элемент к которому применён плагин
$(this).click(function(event)
{
// Благодаря замыканию внутри обработчика
// можно использовать функции и переменные, определённые выше
showMessage(config.text + this.id); //а здесь this -- элемент, на котором кликнули, поэтому this.id вернёт 'clickMe'
event.preventDefault(); //отключаяем переход по ссылке
});
});
}
});
})(jQuery);
jQuery(document).ready(function($){
//Используем плагин
$('#clickMe').clicker({text: 'Клик на элементе: '});
});
Существует несколько вариаций и дополнительных техник для реализации плагинов:
Prototype
Этот фреймворк эмулирует традиционный ООП подход:
// Эмулируем namespace'ы
var CrispStudio = {JSTest : {}};
CrispStudio.JSTest.Clicker = Class.create({
// Эта функция будет "конструктором"
initialize: function(options) {
// "private"-переменные
this.text = options.text;
this.element = $(options.elementId);
// "private"-метод
this.showMessage = function(elementId)
{
alert(this.text + elementId);
};
// Техника т.н. "кеширование обработчиков"
this.doClick = this.doClick.bindAsEventListener(this);
Event.observe(this.element, 'click', this.doClick);
// Альтернатива предыдущим двум строкам (без кеширования обработчиков):
// Event.observe(this.element, 'click', this.doClick.bind(this));
},
// "Public"-методы
// Внутри этой функции this -- объект нашего класса
doClick: function(event)
{
// Получаем id элемента, на котором произошел клик
var clickedElementId = event.element().id;
// Вызов "private"-метода
this.showMessage(clickedElementId);
// Отключаяем переход по ссылке
event.stop();
}
});
document.observe("dom:loaded", function() {
// Создание экземпляра "класса"
new CrispStudio.JSTest.Clicker({elementId: 'clickMe', text: 'Клик на элементе: '});
});
Дополнение от 17.01.2009: В jQuery 1.4 появился метод jQuery.proxy, позволяющий реализовывать что-то подобное на подход Prototype.
Дополнение от 30.09.2010: Обновлён пример для jQuery.
