Javascript的回調,閉包與dojo.hitch

事件驅動機制的重要元素就是回調函數。事件驅動的本質是當程序運行到等待某個資源加載時(比如I/O),並非由程序去輪詢資源的狀態,而是註冊一個消息處理程序(回調函數),當資源可用時(即事件發生時),事件來調用這個消息處理程序以消費該資源。

消息處理程序通常要攜帶參數,否則,它們就只能訪問全局變量,這顯然是不可接受的方式。在C/C++程序中,回調函數的參數通常由一個類型爲(void *)的指針傳遞。在C#中引入了仿函數的概念。仿函數即是把消息處理程序和它所運行所需要的上下文環境包裝在一起成爲一個對象,一起傳遞給消息派發機制,消息派發機制針對該對象調用它的處理函數,這樣,使得回調過程也可以以易於理解的面向對象的處理方式進行。

那麼,在Javascript和dojo中,如何實現普通的回調函數和仿函數功能?先來看一個簡單的例子:

function foo(/*String*/name){
    console.log("hi, how're you?");
}

setTimeout(foo, 2000);

上述示例中,foo就是一個普通的回調函數。在Firebug中運行該程序,firebug控制檯將在2秒鐘以後打印出:how're you?

上面的示例程序有個小小的遺憾,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);

上述示例中'var functor =...'一行聲明瞭一個閉包並立即執行它,從而我們得到了一個指向閉包的對象。有一點值得注意,代碼第5行,"function()"中不可帶有形式參數name,否則第6行foo(name)中的name無法取得正確的值。因爲形式參數的值是在函數調用時賦予的。如果第5行寫成這樣:"return function(name)",顯然這個形參是無法賦值的。

 

閉包能保存其生成時所處上下文的全信息,這些信息包括函數調用參數,本地參數。

再來考察一個複雜一點的例子,使用了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);

使用dojo.hitch來改寫前面的例子:
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
};

上述示例中,有些變量來自於lang.js文件中的定義。比如d是dojo的別名。

dojo.hitch的核心是Function.apply方法。我們直接用apply方法改寫出一個簡單的例子:

var foo = {
     bar: function(a, b, c){
         console.log(a, b, c);
     }
 };
foo.bar.apply(foo, [1,2,3]);

foo.bar是一個Function對象,因此擁有apply方法。注意爲了能保存當前的上下文,dojo.hitch在最後一句中創建了一個閉包並將之返回。

關於dojo.hitch,還有一些事情要指出來,第一,它的method參數可以是Function,也可以是字符串(在scope裏引用到一個函數)。第二,dojo.hitch允許函數除引用scope裏的變量還,還可以臨時指定參數(以數組的形式傳遞進去)。它通過d._hitchArgs函數來完成閉包的生成。第三,註釋裏提到可以用這種方法來模擬帶缺省參數的函數,實際上這種方法並不實用。在註釋的示例中,它實際上是聲明瞭一個函數別名fn,來完成函數foo同樣的事情。在c++/java語言裏,c++直接支持缺省參數,java可以通過override來實現。Javascript裏實現缺省參數函數也應該使用同java一樣的方法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章