Многие знают о механизме Event-Dispatcher-Listener'ов, реализованному во многих языках программирования. Я же создам подобный механизм не для Event'ов, а для любого метода объекта JavaScript — Object.
Я не претендую на оригинальность, нет. Основная цель статьи — рассмотреть интересные механизмы прототипирования в JavaScript, создание декораторов и, собственно, постараться хоть немного раскрыть мощь и гибкость это чудесного языка, который так часто обижают и недооценивают.
UPD1: краткое содержание:
1. Создание декоратора на JavaScript
2. Создание механизма Function call listener с помощью декораторов
UPD2: от 09.06.2009
В конце статьи я добавил раздел Update: Decorator Mark II. В нём — исправления и улучшения (я не перекраивал всю статью из-за этого)
Итак, первый (и, надеюсь, не последний) рецепт в моей поваренной книге JavaScript.
Function Call Listener, фаршированный (многофункциональный), запечённый в собственном соку (без использования каких-то библиотек, на чистом JavaScript).
1. Понимание прототипирования в JavaScript
2. Понимание анонимных функций JavaScript
3. Приблизительное знание класса
Не пугайтесь, я опишу ингредиенты поподробней.
Итак, приступим. Вначале позаимствуем небольшого помощника у John Resig'а (из его книги):
Что делает этот метод? Дословно: он позволяет легко добавлять новый метод в прототип текущей функции.
Как вы знаете, функция в JavaScript является также классом и конструктором класса (не бейте, сдаюсь — нет в JavaScript никаких классов, в привычном понимании).
Верно и следующее: конструктор любого класса — это функция (или по-научному — экземпляр класса
Исходя из принципов прототипирования — после добавления нового метода
Любой класс —
Если считать функцию — привычным классом, то мы по сути добавили статический метод всем-всем-всем классам (JavaScript реально вставляет :-) ).
Ладно, хватит о гениальном. Перейдём теперь к моему коду:
Вуаля! Как я говорил, класс
А значит, как только мы добавили:
У нас тут же появился
После выполнения кода выше у нас появится новый метод
Что делает метод
Конечно этот код можно сильно сократить, но так — более наглядно.
Но это не всё, самое интересное — впереди. Вы думаете я зря назвал мой метод — decorate? Нет, не зря! Это и есть знакомый многим декоратор.
Создадим простенький класс:
Но число ли? Нет, конечно. Я могу передать туда всё, что угодно.
Но что же делать? Мне категорически не хочется менять конструктор. Выход есть: напишем декоратор для ограничения передаваемых параметров и применим его к нашему классу.
Вернёмся к нашему подопытному:
Рассмотрим всё это безобразие повнимательнее. Начнём с момента применения декоратора.
Вот так, вроде бы на основных моментах я заострил внимание.
Ах да, забыл! Как «скинуть» с функции декоратор и вернуть старую? Нет ничего проще:
Теперь продолжим!
Мы плавно подходим к завершению моего рецепта. Еще чуть-чуть специй, и можно в топку… тьфу, в духовку! :)
Как можно догадаться, всё самое важное происходит внутри некой ф-ции
Как добавить listener — понятно. Теперь нужна возможность его «убрать»:
Может этот способ и не оптимальный, я просто взял первый из головы.
Наконец-то, венец этого рецепта, связь декоратора и event-listener схемы — та самая ф-ция
Эту функцию можно условно разделить на блоки.
Блок 1: проверка — не обманывают ли нас, есть ли в этом объекте такой метод?
А в конце видим:
Блок 2: создание соответствующего декоратора, если таковой ещё не определён.
Что же в этом декораторе?
Блок 3: инициализация массивов
Модификация 1: спрячем ф-цию
Модификация 2: загрязнять стандартные классы вроде
Ну и так далее. Как говорится — нет предела совершенству!
Всё! Пирог слеплен, теперь в духовку (т.е. в ваш мозг) на пол-часа, и — готово. Приятного аппетита!
Ну и конкретный пример использования:
Кстати, весь приведённый код — кросс-браузерный, таков мой принцип работы.
Надеюсь что моя стряпня вам понравилась. Пишите, буду рад ответить на вопросы. И поверьте — JavaScript действительно очень мощный, гибкий и класный язык программирования, хоть и скриптовый.
UPD: 09.06.2009
Прошло время с написания статьи и я нашёл в моей реализации ошибки и недостатки. Но я не хочу перекраивать статью, и не хочу создавать новую. Поэтому встречайте здесь и сейчас: Decorator Mark II!
1. Улучшен
2. Улучшен метод
3. Добавлен метод
4. (важно!) Исправлен/изменён метод
Я не претендую на оригинальность, нет. Основная цель статьи — рассмотреть интересные механизмы прототипирования в JavaScript, создание декораторов и, собственно, постараться хоть немного раскрыть мощь и гибкость это чудесного языка, который так часто обижают и недооценивают.
UPD1: краткое содержание:
1. Создание декоратора на JavaScript
2. Создание механизма Function call listener с помощью декораторов
UPD2: от 09.06.2009
В конце статьи я добавил раздел Update: Decorator Mark II. В нём — исправления и улучшения (я не перекраивал всю статью из-за этого)
Итак, первый (и, надеюсь, не последний) рецепт в моей поваренной книге JavaScript.
Блюдо
Function Call Listener, фаршированный (многофункциональный), запечённый в собственном соку (без использования каких-то библиотек, на чистом JavaScript).
Ингредиенты
1. Понимание прототипирования в JavaScript
2. Понимание анонимных функций JavaScript
3. Приблизительное знание класса
Function
Не пугайтесь, я опишу ингредиенты поподробней.
Рецепт
Нырнём на пару метров вглубь
Итак, приступим. Вначале позаимствуем небольшого помощника у John Resig'а (из его книги):
Function.prototype.method = function(methodName, f) {
return this.prototype[methodName] = f;
}
* This source code was highlighted with Source Code Highlighter.
Что делает этот метод? Дословно: он позволяет легко добавлять новый метод в прототип текущей функции.
Как вы знаете, функция в JavaScript является также классом и конструктором класса (не бейте, сдаюсь — нет в JavaScript никаких классов, в привычном понимании).
Верно и следующее: конструктор любого класса — это функция (или по-научному — экземпляр класса
Function
).Исходя из принципов прототипирования — после добавления нового метода
method(...)
в прототип класса Function
, у всех экземпляров класса Function
появился новый метод method(...)
(но помни: в JavaScript нет никаких методов, Нео).Любой класс —
Object
, Array
, Number
, YourClassName
— является экземпляром класса Function
, т.е. просто функцией. А значит у нас появились: Object.method(...)
, Array.method(...)
, YourClassNam.method(...)
Если считать функцию — привычным классом, то мы по сути добавили статический метод всем-всем-всем классам (JavaScript реально вставляет :-) ).
Строим декоратор
Ладно, хватит о гениальном. Перейдём теперь к моему коду:
Function.method("decorate", function(f) {
var oldMe = this;
var newMe = f;
newMe.old = oldMe;
return newMe;
})
* This source code was highlighted with Source Code Highlighter.
Вуаля! Как я говорил, класс
Function
сам по себе является функцией, или экземпляром класса Function
(а-а-а :-))А значит, как только мы добавили:
Function.prototype.method = function(...) {}
* This source code was highlighted with Source Code Highlighter.
У нас тут же появился
Function.method(...)
(уже не в прототипе, а в экземпляре класса Function
)После выполнения кода выше у нас появится новый метод
Function.prototype.decorate(...)
. И опять же — метод появился в прототипе Function
, а значит и во всех-всех-всех классах. Но вот тут как раз мне это не принципиально, а важно лишь присутствие метода decorate(...)
у всех функций.Что делает метод
decorate(...)
?// сохраняем старую функцию
var oldMe = this;
// возвращаем переданную в метод новую функцию, которая "за пазухой" хранит старую.
var newMe = f;
newMe.old = oldMe;
return newMe;
* This source code was highlighted with Source Code Highlighter.
Конечно этот код можно сильно сократить, но так — более наглядно.
Но это не всё, самое интересное — впереди. Вы думаете я зря назвал мой метод — decorate? Нет, не зря! Это и есть знакомый многим декоратор.
Пример декорирования
Создадим простенький класс:
// мега-класс, содержащий число
function MyCoolNumber(initNumber) {
this.value = initNumber;
}
* This source code was highlighted with Source Code Highlighter.
Но число ли? Нет, конечно. Я могу передать туда всё, что угодно.
new MyCoolNumber('на ура') // пройдёт "на ура"
* This source code was highlighted with Source Code Highlighter.
Но что же делать? Мне категорически не хочется менять конструктор. Выход есть: напишем декоратор для ограничения передаваемых параметров и применим его к нашему классу.
function strictArgs() { // здесь будет переменное число аргументов, поэтому я их не именую
var types = arguments; // передаём типы
return function() {
var params = arguments;
if (params.length != types.length)
throw "Ошибка! Ожидалось " + types.length + " аргумент(ов), а пришло " + params.length;
for (var i=0, l=params.length; i < l; i++) {
if (!(params[i] instanceof types[i]) && !(params[i].constructor == types[i]))
throw "Ошибка! Аргумент #" + (i+1) + " должен быть " + types[i].name;
}
arguments.callee.old.apply(this, arguments); // собственно, вызов "старой" функции
}
}
* This source code was highlighted with Source Code Highlighter.
Вернёмся к нашему подопытному:
function MyCoolNumber(initNumber) {
this.value = initNumber;
}
MyCoolNumber = MyCoolNumber.decorate(strictArgs(Number))
new MyCoolNumber(); // Ошибка! Ожидалось 1 аргумент(ов), а пришло 0
new MyCoolNumber(1, 2, 3); // Ошибка! Ожидалось 1 аргумент(ов), а пришло 3
new MyCoolNumber("строка"); // Ошибка! Аргумент #1 должен быть Number
var x = new MyCoolNumber(6); // OK!
alert(x.value) // 6, что и следовало ожидать, значит декоратор отработал нормально.
* This source code was highlighted with Source Code Highlighter.
Рассмотрим всё это безобразие повнимательнее. Начнём с момента применения декоратора.
- вызывается ф-ция
strictArgs
с одним аргументом —Number
var types = arguments; // мы сохраняем аргументы вызова функции strictArgs(...) в переменную types, которая по сути содержит сейчас [Number] - массив из 1 эл-та: класса Number. Конечно объект типа Arguments - это не массив, но сейчас не об этом, и будем считать его массивом
strictArgs(...)
возвращает новую ф-цию, внутри которой:
var params = arguments; // мы точно так же сохраняем аргументы вызова, но уже аргументы, переданные в наш будущий обновлённый MyCoolNumber;
- начинаем сравнивать эти 2 массива на совпадение размерностей и типов
- мы декорируем ф-цию
MyCoolNumber
той, которая вернулась изstrictArgs(...)
Заметим: связь оригинальной ф-ции с декоратором осуществляется черезarguments.callee.old.apply(this, arguments)
:
arguments
— стандартный объект для описания аргументов вызываемой функцииarguments.callee
— сама функция-декораторarguments.callee.old
— помните, что такое — old? Когда мы передаём функцию-декоратор в метод decorate(...), он добавляет этой ф-ции атрибут old, ссылающийся на «старую» ф-циюarguments.callee.old.apply(...)
— стандартный метод класса Function. Не буду о нём, скажу лишь, что он вызывает ф-цию с заданным scope и argumentsarguments.callee.old.apply(this, arguments)
— собственно, подтверждение вышесказанного
Вот так, вроде бы на основных моментах я заострил внимание.
Ах да, забыл! Как «скинуть» с функции декоратор и вернуть старую? Нет ничего проще:
Function.method("recover", function() {
return this.old || this;
})
* This source code was highlighted with Source Code Highlighter.
Теперь продолжим!
Смотрим на объект, слушаем методы
Мы плавно подходим к завершению моего рецепта. Еще чуть-чуть специй, и можно в топку… тьфу, в духовку! :)
Object.method('before', function(methodName, f){
var method = listenerInit.call(this, methodName);
if (method)
method.listenersBefore.push(f);
})
Object.method('after', function(methodName, f){
var method = listenerInit.call(this, methodName);
if (method)
method.listenersAfter.push(f);
})
* This source code was highlighted with Source Code Highlighter.
Как можно догадаться, всё самое важное происходит внутри некой ф-ции
listenerInit(...)
, но о ней — позже. Пока-что просто поверим, что она делает все необходимые приготовления.Как добавить listener — понятно. Теперь нужна возможность его «убрать»:
Object.method('removeBefore', function(methodName, f){
var method = listenerInit.call(this, methodName);
if (method) {
var _nl = [];
while (method.listenersBefore.length) {
var _f = method.listenersBefore.shift();
if (_f != f)
_nl.push(_f);
}
method.listenersBefore = _nl;
}
})
Object.method('removeAfter', function(methodName, f){
var method = listenerInit.call(this, methodName);
if (method) {
var _nl = [];
while (method.listenersAfter.length) {
var _f = method.listenersAfter.shift();
if (_f != f)
_nl.push(_f);
}
method.listenersAfter = _nl;
}
})
* This source code was highlighted with Source Code Highlighter.
Может этот способ и не оптимальный, я просто взял первый из головы.
Наконец-то, венец этого рецепта, связь декоратора и event-listener схемы — та самая ф-ция
listenerInit
:function listenerInit(methodName) {
var method = this[methodName];
if (typeof method != "function")
return false;
// продекорировано, или ещё нет?
if (!method.listenable) {
this[methodName] = method.decorate(function(){
var decorator = arguments.callee;
decorator.listenable = true;
var list = decorator.listenersBefore;
for (var i = 0, l = list.length; i < l; i++) {
if (typeof list[i] == "function" && list[i].apply(this, arguments) === false)
return;
}
var ret = decorator.old.apply(this, arguments);
list = decorator.listenersAfter;
for (var i = 0, l = list.length; i < l; i++)
list[i].apply(this, arguments);
return ret;
});
method = this[methodName];
}
method.listenersBefore = method.listenersBefore instanceof Array ? method.listenersBefore : [];
method.listenersAfter = method.listenersAfter instanceof Array ? method.listenersAfter : [];
return method;
}
* This source code was highlighted with Source Code Highlighter.
Эту функцию можно условно разделить на блоки.
Блок 1: проверка — не обманывают ли нас, есть ли в этом объекте такой метод?
var method = this[methodName];
if (typeof method != "function")
return false;
* This source code was highlighted with Source Code Highlighter.
А в конце видим:
return method
, т.е. listenerInit(...)
возвращает либо false
, либо уже «украшенный» метод.Блок 2: создание соответствующего декоратора, если таковой ещё не определён.
Что же в этом декораторе?
- Запускаем все listener'ы из массива
listenersBefore
. Если хоть 1 из них возвращаетBoolean false
— прекращаем выполнение - Вызов базового метода
- Запускаем все listener'ы из массива
listenersAfter
- Декоратор возвращает то значение, которое вернул базовый метод
Блок 3: инициализация массивов
method.listenersBefore
и method.listenersAfter
.Плюшки
Модификация 1: спрячем ф-цию
listenerInit
с глаз долой. Для этого используем JavaScript-замыкание:(function(){
// listenerInit(...) и все-все-все
// .....
})()
* This source code was highlighted with Source Code Highlighter.
Модификация 2: загрязнять стандартные классы вроде
Object
— очень плохо, поэтому можно модифицировать ваш конкретный класс:YourClass.method('before', function(methodName, f){
var method = listenerInit.call(this, methodName);
if (method)
method.listenersBefore.push(f);
})
* This source code was highlighted with Source Code Highlighter.
Ну и так далее. Как говорится — нет предела совершенству!
Всё! Пирог слеплен, теперь в духовку (т.е. в ваш мозг) на пол-часа, и — готово. Приятного аппетита!
Как это есть
Ну и конкретный пример использования:
// Создадим простенький класс
var Num = function(x) {
this.x = x;
}
// Опишем прототип
Num.prototype.x = null;
Num.prototype.getX = function() {
return this.x;
};
Num.prototype.setX = function(x) {
return this.x = x;
}
// Создадим экземпляр
var t = new Num(6);
// Добавим слушателя after
t.after("getX", function(){
alert('Поздравляем! Ваш X == ' + this.x + '!');
})
// Добавим слушателя after
t.after("getX", function(){
alert('И ещё раз поздравляем!');
})
// Добавим слушателя before, с проверкой
var f = function(x){
if (x < 0 || x > 10) {
alert('Нет! Значение должно быть в отрезке [0, 10]');
return false;
}
}
t.before("setX", f)
// поиграем:
t.getX(); // Поздравляем! Ваш X == 6! -> 'И ещё раз поздравляем! -> вызов базового getX(...)
t.setX(100); // Нет! Значение должно быть в отрезке [0, 10] -> базовый setX(100) - не вызвался
alert(t.x); // 6
t.setX(4); // Всё ОК, вызывается базовый метод setX(4)
alert(t.x); // 4
t.removeBefore("setX", f) // удаляем нашу проверку f(...)
t.setX(100); // всё ОК, сработал базовый setX(100)
alert(t.x); // 100
* This source code was highlighted with Source Code Highlighter.
Кстати, весь приведённый код — кросс-браузерный, таков мой принцип работы.
Вывод
Надеюсь что моя стряпня вам понравилась. Пишите, буду рад ответить на вопросы. И поверьте — JavaScript действительно очень мощный, гибкий и класный язык программирования, хоть и скриптовый.
Update: Decorator Mark II
UPD: 09.06.2009
Прошло время с написания статьи и я нашёл в моей реализации ошибки и недостатки. Но я не хочу перекраивать статью, и не хочу создавать новую. Поэтому встречайте здесь и сейчас: Decorator Mark II!
1. Улучшен
Function.method(...)
// Теперь мы можем как задавать метод, так и доставать его
Function.prototype.method = function(methodName, f) {
if (typeof f != "undefined")
this.prototype[methodName] = f;
return this.prototype[methodName];
}
* This source code was highlighted with Source Code Highlighter.
2. Улучшен метод
restore(...)
// Теперь мы можем достать как ближайшую "украшенную" ф-цию, так и самую первую в цепочке
Function.method("restore", function(fullRestore){
var ret = this.old || this;
while (fullRestore && ret.old) {
ret = ret.old;
}
return ret;
})
* This source code was highlighted with Source Code Highlighter.
3. Добавлен метод
decorateMethod(...)
// специально для декорирования методов
Function.method("decorateMethod", function(methodName, decorator){
var f = this.method(methodName);
if (!f)
return null;
f.name = methodName;
f = f.decorate(decorator);
return this.method(methodName, f);
})
* This source code was highlighted with Source Code Highlighter.
4. (важно!) Исправлен/изменён метод
decorate(...)
Function.method("decorate", function(decorator){
// Сохраняем исходную функцию
var oldFunc = this;
// Важно! теперь возможно нормальное пере-использование одного и того же декоратора.
// Теперь мы его никак не трогаем и не изменяем, а создаём и возвращаем новую ф-цию.
// Однако теперь декоратор первым аргументом всегда будет получать некий объект,
// в котором -- original: oldFunc (оригинал) и decoratorInstance: f (настоящий декоратор)
var f = function(){
return decorator.apply(this, [{original: oldFunc, decoratorInstance: f}].concat([].slice.apply(arguments)))
}
// Сохраняем оригинал ф-ции - в decoratorInstance f
f.old = oldFunc;
// Восстанавливаем прототип и конструктор ф-ции.
// Это необходимо для сохранения ф-ции как конструктора.
f.prototype = this.prototype;
f.prototype.constructor = f;
// Восстанавливаем имя ф-ции. Откуда оно вообще берётся? Можно задать вручную.
//Или см. выше, новый метод decorateMethod: в нём это задаётся.
f.name = oldFunc.name;
// возвращаем декоратор
return f;
})
* This source code was highlighted with Source Code Highlighter.
Если вы решите попробовать эти изменения (особенно касающиесь метода decorate(...)), то вам необходимо будет исправить примеры из этой статьи.
К примеру:
// По-старому
function strictArgs() {
var types = arguments;
return function() {
var params = arguments;
//...
return arguments.callee.old.apply(this, arguments);
}
}
* This source code was highlighted with Source Code Highlighter.
надо изменить на:
// По-новому
function strictArgs() {
var types = arguments;
return function(dScope) {
var original = arguments[0].original; // или можно dScope.original
var arguments = Array.prototype.slice.call(arguments, 1);
var params = arguments;
//...
return original.apply(this, arguments);
}
}
* This source code was highlighted with Source Code Highlighter.
Мои извинения, чтиво стало ещё длинней :)