一直很感兴趣对于JavaScript这样的动态类型语言,没有强类型也没有接口要怎么通过设计模式写出优雅的代码。这次一口气读完了腾讯出的一本关于JS设计模式的书,非常有启发,这里写一个小的系列给大家分享。还是那句话,设计是为了更好的解决问题,而不是为了设计而设计,所以重要的是理解思想,而不是当成一个模板去套。毕竟设计模式会带来性能的损耗和他人阅读代码学习成本的上升,如非必要,勿增实体,度的把握才是我们在实践中要去不断探索的。
还有一些最近学习的心得也在上代码前先在这里说一下:
1、分辨模式的关键是意图而不是结构。
在设计模式的学习中,有人经常发出这样的疑问:代理模式和装饰者模式,策略模式和状态模式,策略模式和智能命令模式,这些模式的类图看起来几乎一模一样,它们到底有什么区别?实际上这种情况是普遍存在的,许多模式的类图看起来都差不多,模式只有放在具体的环境下才有意义。比如我们的手机,把它当电话的时候,它就是电话;把它当闹钟的时候,它就是闹钟;用它玩游戏的时候,它就是游戏机。有很多模式的类图和结构确实很相似,但这不太重要,辨别模式的关键是这个模式出现的场景,以及为我们解决了什么问题。
2、JS的设计模式不应该是静态语言设计模式的生拉硬套。
在JavaScript这种类型模糊的语言中,对象多态性是天生的,一个变量既可以指向一个类,又可以随时指向另外一个类。JavaScript不存在类型耦合的问题,自然也没有必要刻意去把对象“延迟”到子类创建,也就是说, JavaScript实际上是不需要工厂方法模式的。模式的存在首先是能为我们解决什么问题,这种牵强的模拟只会让人觉得设计模式既难懂又没什么用处。
3、duck typing
动态类型语言对变量类型的宽容给实际编码带来了很大的灵活性。由于无需进行类型检测,我们可以尝试调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法。
这一切都建立在鸭子类型(duck typing)的概念上,鸭子类型的通俗说法是:“ 如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。鸭子类型指导我们只关注对象的行为,而不关注对象本身,也就是关注 HAS-A, 而不是 IS-A。
4、基于鸭子类型的动态类型语言的面向对象设计
鸭子类型的概念至关重要。利用鸭子类型的思想,我们不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:“面向接口编程,而不是面向实现编程”。例如,一个对象若有 push 和 pop 方法,并且这些方法提供了正确的实现,它就可以被当作栈来使用。一个对象如果有 length 属性,也可以依照下标来存取属性(最好还要拥
有 slice 和 splice 等方法),这个对象就可以被当作数组来使用。
在静态类型语言中,要实现“面向接口编程”并不是一件容易的事情,往往要通过抽象类或者接口等将对象进行向上转型。当对象的真正类型被隐藏在它的超类型身后,这些对象才能在类型检查系统的“监视”之下互相被替换使用。只有当对象能够被互相替换使用,才能体现出对象多态性的价值。
“面向接口编程”是设计模式中最重要的思想,但在 JavaScript 语言中,“面向接口编程”的过程跟主流的静态类型语言不一样,因此,在 JavaScript 中实现设计模式的过程与在一些我们熟悉的语言中实现的过程会大相径庭。
PS:本文默认你已经是一个JS熟手,对于JS的this,call,apply,原型,闭包等重要概念已经有起码的理解,如果不太熟悉,建议学习后再来尝试阅读本文。另外本文核心关注的是JS怎样实现对应的模式,所以不再会赘述设计模式本身的相关知识,有需要的同学可以去参考对应的书籍。
//惰性加载模式,实例在需要的时候才会创建
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ));
}
};
Example:
//JQuery
var bindEvent = function(){
$( 'div' ).one( 'click', function()
{
alert ( 'click' );
});
};
//Our Impl
var bindEvent = getSingle(function()
{
document.getElementById( 'div1' ).onclick = function()
{
alert ( 'click' );
}
//注意这里必须有返回
return true;
});
在 getSinge 函数中,实际上也提到了闭包和高阶函数的概念。单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。
var StrategyA = function (data)
{
console.log("strategy A dispose " + data);
}
var StrategyB = function (data)
{
console.log("strategy B dispose " + data);
}
var StrategyC = function (data)
{
console.log("strategy C dispose " + data);
}
var Invoker = function (strategy, data)
{
return strategy(data);
}
var context = function (data)
{
if(data > 3)
{
return new Invoker(StrategyA, data);
}
else if(data > 1 && data <= 3)
{
return new Invoker(StrategyB, data);
}
else
{
return new Invoker(StrategyC, data);
}
}
context(5);
Peter Norvig 在他的演讲中曾说过:“在函数作为一等对象的语言中,策略模式是隐形的。strategy 就是值为函数的变量。”在JavaScript 中,除了使用类来封装算法和行为之外,使用函数当然也是一种选择。这些“算法”可以被封装到函数中并且四处传递,也就是我们常说的“高阶函数”。实际上在 JavaScript 这种将函数作为一等对象的语言里,策略模式已经融入到了语言本身当中,我们经常用高阶函数来封装不同的行为,并且把它传递到另一个函数中。当我们对这些函数发出“调用”的消息时,不同的函数会返回不同的执行结果。在 JavaScript 中,”函数对象的多态性“来的更加简单。所以,在 JavaScript 语言的策略模式中,策略类往往被函数所代替,这时策略模式就成为一种“隐形”的模式。
var before = function (fn, fnBefore)
{
var self = this;
return function ()
{
fnBefore.apply(self, arguments);
return fn.apply(self, arguments);
}
}
var after = function (fn, fnAfter)
{
var self = this;
return function ()
{
var ret = fn.apply(self, arguments);
fnAfter.apply(self, arguments);
return ret;
}
}
var test = function ()
{
console.log("test");
}
test = before(test, function ()
{
console.log("before");
});
test = after(test, function ()
{
console.log("after");
});
test();
这里我们直接引入了JS的AOP,代理模式是一个非常强大的模式,虽然它的原理很简单。几乎所有的hack技术,本质都是proxy一个原系统中的类或方法,然后植入自己想要做的事情。其他的proxy模式应用,一般需要通过具体的应用场景来实现,这里就不再一一列举。
//内部迭代器
var each = function (arr, callback)
{
for (var i = 0; i < arr.length; i++)
{
callback.call(arr[i], i, arr[i]);
}
}
each([1, 2, 3], function (i, n)
{
console.log([i, n]);
});
//外部迭代器,必须显式地请求迭代下一个元素,可以手工控制迭代过程和顺序
var Iterator = function (obj)
{
var current = 0;
var next = function ()
{
current += 1;
}
var isDone = function ()
{
return current >= obj.length;
}
var getCurrentItem = function ()
{
return obj[current];
}
return {
next : next,
isDone : isDone,
getCurrentItem : getCurrentItem
}
}
var compare = function (iterator1, iterator2)
{
while (!iterator1.isDone() && !iterator2.isDone())
{
if(iterator1.getCurrentItem() != iterator2.getCurrentItem())
{
throw new Error("not equals");
}
iterator1.next();
iterator2.next();
}
alert("equals");
}
var iterator1 = Iterator([1, 2, 3]);
var iterator2 = Iterator([1, 2, 4]);
compare(iterator1, iterator2);
迭代器模式是一种相对简单的模式,简单到很多时候我们都不认为它是一种设计模式。目前的绝大部分语言都内置了迭代器。
var Observable = (function ()
{
var global = this;
var Observable;
var _default = "default";
Observable = (function ()
{
var _addListener;
var _notify;
var _removeListener;
var _slice = Array.prototype.slice;
var _shift = Array.prototype.shift;
var _unshift = Array.prototype.unshift;
var namespaceCache = {};
var _create;
var find;
var each;
each = function (arr, fn)
{
var ret;
for(var i = 0; i < arr.length; i++)
{
var n = arr[i];
ret = fn.call(n, i , n);
}
return ret;
};
_addListener = function (key, listener, cache)
{
if(!cache[key])
{
cache[key] = [];
}
cache[key].push(listener);
};
_removeListener = function (key, cache, listener)
{
if(cache[key])
{
if(listener)
{
for(var i = 0; i < cache[key].length; i++)
{
if(listener === cache[key][i])
{
cache[key].splice(i, 1);
}
}
}
else
{
cache[key] = [];
}
}
};
_notify = function ()
{
// var obs = _shift.call(arguments);
// var _arguments = _shift.call(arguments);
var cache = _shift.call(arguments);
var key = _shift.call(arguments);
var args = arguments;
var _self = this;
var listeners = cache[key];
if(!listeners || !listeners.length)
{
return;
}
return each(listeners, function ()
{
//this refer to a listener
return this.apply(_self, args);
});
};
_create = function (namespace)
{
var namespace = namespace || _default;
var cache = {};
var offlineStack = [];
var ret = {
addListener : function (key, listener, last)
{
_addListener(key, listener, cache);
if(offlineStack === null)
{
return;
}
if(last === "last")
{
offlineStack.length && offlineStack.pop();
}
else
{
each(offlineStack, function ()
{
this();
});
}
offlineStack = null;
},
//make the key of the listeners with the only one
one : function (key, listener, last)
{
_removeListener(key, cache);
this.addListener(key, listener, last);
},
removeListener : function (key, listener)
{
_removeListener(key, cache, listener);
},
notify : function ()
{
var fn;
var args;
var _self = this;
_unshift.call(arguments, cache);
args = arguments;
fn = function ()
{
return _notify.apply(_self, args);
}
if(offlineStack)
{
return offlineStack.push(fn);
}
return fn();
},
};
return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
};
return {
create : _create,
one : function (key, listener, last)
{
var observable = this.create();
observable.one(key, listener, last);
},
addListener : function (key, listener, last)
{
var observable = this.create();
observable.addListener(key, listener, last);
},
removeListener : function (key, listener)
{
var observable = this.create();
observable.removeListener(key, listener);
},
notify : function ()
{
var observable = this.create();
observable.notify.apply(this, arguments);
}
};
})();
return Observable;
})();
Observable.notify("msg", 1000);
Observable.addListener("msg", function (data)
{
alert("final msg " + data);
});
其实看书前我也没想到静态语言中非常简单的观察者模式在js里可以写这么复杂,不过这个实现方法是同时考虑了“离线消息”的,也就是说框架可以先发布消息,然后后来的订阅者一样可以收到消息。实际开发中如果不存在这样的业务可以把这个代码做一个简化。
这篇先说到这。