27 июля 2009, 23:59

Hard JavaScript: декораторы (decorators) Избранное

Декоратор — образно говоря, это нечто, что оборачивает другое нечто %))) Вот например есть функция и мы можем создать для неё декоратор, который будет запускать какую-то другую функцию в начале или в конце выполнения «родителя». Для чего-же?: вариантов миллион ;) Например для проверки данных, если этой проверки ранние не было, или для логгирования, дебагинга, или для изменения общего поведения ;)


Перейдём к делу %) Создадим помощник, который будет нам помогать делать чудеса.
Function.prototype.method = function(methodName, f) {
  if (typeof f != "undefined")
    this.prototype[methodName] = f;
  return this.prototype[methodName];
}
Что-что? Спросите вы! Образно говоря, мы легко добавляем новый (если существует, возвращаем старый) метод (функцию) в прототип заданной функции о_О Прототип?? Хе-хе! Прототип — это свойства и методы функции, класса, массива, любого объекта, которые существуют по умолчанию. :D Да, они ограничены (прелести в прототипе), и их есть стандартный набор, потому мы можем их добавлять переназначать или удалять ;)


Приступим к декораторам ;)
// Теперь мы можем достать как ближайшую "украшенную" ф-цию, так и самую первую в цепочке
Function.method("restore", function(fullRestore){
  var ret = this.old || this;
  while (fullRestore && ret.old) {
    ret = ret.old;
  }
  return ret;
})
// специально для декорирования методов, востановления имён
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);
})
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;
})
Всем понравилось, теперь пример
// Простая функция-декоратор		
function docwrite() {
	var types = arguments; // передаём типы
	return function(dScope) {
		// получаем аргументы родителя (воруем)
		var original = arguments[0].original; // или можно dScope.original
		var arguments = Array.prototype.slice.call(arguments, 1);
		var params = arguments;
		// выводим наш текст, который мы алертим
		document.write(params[0]);
		
		// незабываем позвать родителя, а то не красиво ;)
		return original.apply(this, arguments);
		// в результате видем ещё и алерт
	}
}
// надеваем декоратор на алерт
alert = alert.decorate(docwrite());
Дальше в body пишем
<script type="text/javascript">
  alert("y0");
</script>



Теперь бонусы
// Кикнуть декоратор можно так ;)
Function.method("recover", function() {
  return this.old || this;
})
// Повесить обработку "до"
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);
})
// что бы повесить декоратор "до" и "после", нам нужен вот такой обработчик (слушатель)
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;
}


3 комментария РСС

CTAPbIu_MABP
Хорошая статья, мало кто так о JS пишет особенно в рунете
Vitaliy Bogdanets
Спасибо, буду стараться ;)

Изначально блог задумывался как «для себяшный сборник знаний», но на него начал ходить народ ;)
alex_past
Спасибо. Неплохо написано.
Но у нас отродясь это не называли декораторами. По-русски принято называть их обертками.
Vitaliy Bogdanets
decoration = украшение = оборачивание
* всё одно и тоже ;)
johny24
Можете подсказать, или источником поделиться, где объясняется, что это за запись такая Function.prototype.method, почему Function с большой буквы,

Ваш комментарий

адрес не будет опубликован

ХТМЛ не работает


Ctrl + Enter