早些日子审查同事的一段代码--用Javascript实现的类似WindowForm下的MessageBox,有一段代码大致是这样的(这是示意代码,仅保留了和问题相关的部分):
MessageBox.prototype = {
…
// 方法定义
Show : function(callback,requester,…){
…
// 这里实际上是异步调用,单击按钮后触发。
callback(requester, result);
…
}
}
// 调用显示MessageBox的方法
Message.Show(callbackFunc, this);
这里的requester,和MessageBox没有任何关系,其实是callback所需要的参数,用于显性的告知callback请求回调时的上下文。这和Javascript的作用域有关,网上谈得很多,傅小康也整理过一篇文档,这里不赘述了。这是一个不差的做法。jQuery中也大量充斥了类似代码,例如jQuery.ajax有一个参数context,就是类似的作用(jQuery内部会以此重设回调函数的上下文)。
虽然被普遍接受,但是总有一些不和谐的地方:既然requester仅仅是callback关注的内容,那么为什么要通过MessageBox来传递呢,MessageBox引用了和它完全无关的东西。用《重构》的话来讲,这就是Smell!
其实有更雅致的做法,调用者在传递callback的时候,就指定了callback的上下文,修改后的定义就很简洁了:
MessageBox.prototype = {
…
// 方法定义
Show : function(callback,…){
…
// 实际上是在用于单击按钮后调用
callback(result);
…
}
}
调用的地方也需要稍作修改:
Message.Show(callbackFunc.bind(this));
bind的操作结果就是返回了一个工作在指定上下文下的新的函数实例,我们应该把它作为Javascript中异步调用的标准模式。
(BTW:有关Javascript的API,还是MSDN比较靠谱,也比W3School中完整。)
由此也可以引申一个比较有趣的话题,Javascript是面向对象的范式语言么?
过去我一直觉得是的,源于外界刻意强调Javascript的某些类似OO的特性,例如:原型继承、通过构造函数定义类型等。其实并非如此,this就暴露了一切根源,Javascript的方法(函数)和定义时绑定的对象是没有强关联的,仅和调用上下文相关,可以随时改变绑定的对象,例如bind、call、apply等一系列方法就是起这个作用的。
Javascript是面向函数的范式语言,它封装的单位并不是类(Type),而是闭包(Closure)也就是调用函数和它的上下文。按照这个思路去理解的话,对于回调函数,我们传递的不仅仅是函数本身,还应该包括它执行时的上下文,所以调用bind等于创建了一个闭包,它才是在Javacript下封装的正确做法。理解这一点,才能更好的理解Javascript这门语言。
还想再引申一下,大概可以上升为设计模式的一点东西。
我们在封装的时候会考虑两类事物:数据和行为。对于面向对象的范式而言,数据和行为是一起定义的,在需要的时候创建一个包含数据和行为的实例;对于面向函数的范式,分别定义数据和行为(PlainObject和Function),在需要的时候将行为绑定到数据上(如Javacript中的call和apply)。