事件驱动机制的重要元素就是回调函数。事件驱动的本质是当程序运行到等待某个资源加载时(比如I/O),并非由程序去轮询资源的状态,而是注册一个消息处理程序(回调函数),当资源可用时(即事件发生时),事件来调用这个消息处理程序以消费该资源。
消息处理程序通常要携带参数,否则,它们就只能访问全局变量,这显然是不可接受的方式。在C/C++程序中,回调函数的参数通常由一个类型为(void *)的指针传递。在C#中引入了仿函数的概念。仿函数即是把消息处理程序和它所运行所需要的上下文环境包装在一起成为一个对象,一起传递给消息派发机制,消息派发机制针对该对象调用它的处理函数,这样,使得回调过程也可以以易于理解的面向对象的处理方式进行。
那么,在Javascript和dojo中,如何实现普通的回调函数和仿函数功能?先来看一个简单的例子:
function foo(/*String*/name){
console.log("hi, how're you?");
}
setTimeout(foo, 2000);
上面的示例程序有个小小的遗憾,setTimeout只接受两个参数,因些它不能将foo函数需要的name参数传递进去。这里似乎是仿函数的概念可以起作用的地方。如果我们可以将一个对象传递给setTimeout,而这个对象又能象函数一样调用,那么似乎问题就可以解决了。Javascript的世界里没有仿函数这个概念,取而代之的是外延更宽广的闭包概念:
function foo(/*String*/name){
console.log("hi " + name + ", how're you?");
}
var functor = function(name){
return function(){
foo(name);
}
}('aaron');
setTimeout(functor , 500);
闭包能保存其生成时所处上下文的全信息,这些信息包括函数调用参数,本地参数。
再来考察一个复杂一点的例子,使用了dojo的API:
var args = {
url: "foo",
load: this.dataLoaded
}
dojo.xhrGet(args);
这段代码首先定义了dojo.xhrGet所需要的参数。xhrGet是dojo封装的XMLHttpRequest请求中的HTTP GET请求。它的参数中,url是要访问的资源的URL,'load'则是当该资源获取到本地后需要调用的回调函数。当定义变量var args时,显然this是等同于args对象的。问题是,dojo.xhrGet取回资源,再来调用this.dataLoaded函数时,此时的this是xhrGet运行环境下的'this',并非args变量本身。【关于this:当代码处于定层位置时,this指的是宿主对象,在浏览器中即为window对象。当代码处于字面量对象定义当中(如本例)时,'this'相当于该字面量对象】。上述示例中即使是使用args.dataLoaded也是不行的,因为在xhrGet的运行环境下,仍然有可能不在args的作用域范围内。
显然,这时应该使用闭包,将回调函数及其运行环境封装起来一起传递给dojo.xhrGet().当然可以使用前面生成闭包的技巧,但dojo提供了一个漂亮的方法:dojo.hitch
var args = {
url: "foo",
load: dojo.hitch(this, "dataLoaded")
}
dojo.xhrGet(args);
var obj = {
name : 'aaron',
method: function(){
console.log("hi " + this.name + ", how're you?");
}
}
var functor = dojo.hitch(obj, "method");
setTimeout(functor , 500);
dojo.hitch作用于对象,因此首先要将方法及其访问的上下文包装成一个对象。这在面向对象的编程方式中是很常见的。对于method方法的定义,同样要注意这里定义时,不能带形式参数。method要访问的数据只能通过成员变量来获取,其次,引用成员变量时,要加上'this'关键字。否则,console.log语句会在全局作用域中寻找name。
最后,调用dojo.hitch时,一定要写成示例那样,而不能写成dojo.hitch(obj, method)。如此一来dojo会在全局作用域中去寻找method方法,从而导致出错。
最后,我们来看看dojo是如何实现hitch方法的(dojo/_base/lang.js):
dojo.hitch = function(/*Object*/scope, /*Function|String*/method /*,...*/){
// summary:
// Returns a function that will only ever execute in the a given scope.
// This allows for easy use of object member functions
// in callbacks and other places in which the "this" keyword may
// otherwise not reference the expected scope.
// Any number of default positional arguments may be passed as parameters
// beyond "method".
// Each of these values will be used to "placehold" (similar to curry)
// for the hitched function.
// scope:
// The scope to use when method executes. If method is a string,
// scope is also the object containing method.
// method:
// A function to be hitched to scope, or the name of the method in
// scope to be hitched.
// example:
// | dojo.hitch(foo, "bar")();
// runs foo.bar() in the scope of foo
// example:
// | dojo.hitch(foo, myFunction);
// returns a function that runs myFunction in the scope of foo
// example:
// Expansion on the default positional arguments passed along from
// hitch. Passed args are mixed first, additional args after.
// | var foo = { bar: function(a, b, c){ console.log(a, b, c); } };
// | var fn = dojo.hitch(foo, "bar", 1, 2);
// | fn(3); // logs "1, 2, 3"
// example:
// | var foo = { bar: 2 };
// | dojo.hitch(foo, function(){ this.bar = 10; })();
// execute an anonymous function in scope of foo
if(arguments.length > 2){
return d._hitchArgs.apply(d, arguments); // Function
}
if(!method){
method = scope;
scope = null;
}
if(d.isString(method)){
scope = scope || d.global;
if(!scope[method]){ throw(['dojo.hitch: scope["', method, '"] is null (scope="', scope, '")'].join('')); }
return function(){ return scope[method].apply(scope, arguments || []); }; // Function
}
return !scope ? method : function(){ return method.apply(scope, arguments || []); }; // Function
};
dojo.hitch的核心是Function.apply方法。我们直接用apply方法改写出一个简单的例子:
var foo = {
bar: function(a, b, c){
console.log(a, b, c);
}
};
foo.bar.apply(foo, [1,2,3]);
关于dojo.hitch,还有一些事情要指出来,第一,它的method参数可以是Function,也可以是字符串(在scope里引用到一个函数)。第二,dojo.hitch允许函数除引用scope里的变量还,还可以临时指定参数(以数组的形式传递进去)。它通过d._hitchArgs函数来完成闭包的生成。第三,注释里提到可以用这种方法来模拟带缺省参数的函数,实际上这种方法并不实用。在注释的示例中,它实际上是声明了一个函数别名fn,来完成函数foo同样的事情。在c++/java语言里,c++直接支持缺省参数,java可以通过override来实现。Javascript里实现缺省参数函数也应该使用同java一样的方法。