一、javascript的高級特性--閉包
理解closure前,首先明白函數作用域的概念。也就是全局變量和局部變量。在函數體內部可以調用全局變量,而函數體內被申明的變量不能被外部調用。
比較特殊的是,在js中,函數內部聲明變量的時候,一定要使用var命令。如果不用的話,你實際上聲明了一個全局變量!
其次,要理解javascript中的垃圾回收機制,在Javascript中,如果一個對象不再被引用,那么這個對象就會被GC回收。如果兩個對象互相引用,而不再被第3者所引用,那么這兩個互相引用的對象也會被回收。
了解這兩個概念后,我們在看閉包。
“官方”給出的解釋是:閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
這句話通俗的來說就是:JavaScript中所有的function都是一個閉包。不過一般來說,嵌套的function所產生的閉包更為強大,也是大部分時候我們所謂的“閉包”。看下面這段解釋閉包的經典代碼:
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c();
在這里,a被b引用了。而b又被c引用了。這是對象不會被gc回收。這就形成了所謂的閉包。也就是說,在a執行完並返回后,閉包使得Javascript的垃圾回收機制GC不會收回a所占用的資源,因為a的內部函數b的執行需要依賴a中的變量。
好了,深入理解之。再引入一片概念(copy....)
函數的執行環境(execution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數a從定義到執行的過程為例闡述這幾個概念。
- 當定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)設置為定義a時a所在的“環境”,如果a是一個全局函數,則scope chain中只有window對象。
- 當執行函數a的時候,a會進入相應的執行環境(excution context)。
- 在創建執行環境的過程中,首先會為a添加一個scope屬性,即a的作用域,其值就為第1步中的scope chain。即a.scope=a的作用域鏈。
- 然后執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型而且不能通過JavaScript代碼直接訪問。創建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。
- 下一步是在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
- 最后把所有函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,因此如同第3步,函數b的作用域鏈被設置為b所被定義的環境,即a的作用域。
到此,整個函數a從定義到執行的步驟就完成了。此時a返回函數b的引用給c,又函數b的作用域鏈包含了對函數a的活動對象的引用,也就是說b可以訪問到a中定義的所有變量和函數。函數b被c引用,函數b又依賴函數a,因此函數a在返回后不會被GC回收。
在理解原理之后,我們來看看引用場景。
1.保護了函數a的安全性。
2.在內存中維持一個變量。
3.通過保護變量的安全實現JS私有屬性和私有方法。
二、dojo.hitch()
看完閉包后,再來說說,我寫本文的目的。dojo.hitch(scope,method).
官方解釋是:Dojo.hitch is a neat function. It returns a function that will execute a given function in a given scope. This function allows you to control how a function executes, particularly in asynchronous operations。
看以下代碼:
var args = { url: "foo", load: this.dataLoaded }; dojo.xhrGet(args);
這段代碼執行會怎么樣呢?會報錯。說dataLoaded不是個方法,或找不到。為什么呢。從代碼中可以看出參數args的load想調用外部一個dataLoaded的方法。但是this引用卻指向了args。而args里面可沒有dataLoaded。
如何解決呢?
var args = { url: "foo", load: dojo.hitch(this, "dataLoaded") }; dojo.xhrGet(args);
用dojo.hitch來控制dataLoaded函數執行的作用域為this,這個問題也就迎刃而解了。
說到了hitch,我們就來看看它在dojo庫里是怎么實現的?
在說原理之前,還是一樣先引入兩個javascript 的方法, Function.apply(obj,args) 和 Function.call(obj,[param1[,param2[,…[,paramN]]]])。
這兩個函數所做的事情沒有太多差別,差異在於他們傳入的參數,一個是args任意參數,一個是數組的參數。
大致用法如下,obj1.method.call(obj2,argument1...),
call的作用就是把obj1的方法放到obj2上使用,后面的argument1..這些做為其他參數傳入。也可以說是obj1能夠劫持obj2的方法並繼承obj2的屬性。
繼承?對,就是繼承。我們可以這么做。
<script> function Animal() { this.name = 'Animal'; this.sleep = function(who) { alert(who + " sleep!!"); } } function Dog(){ Animal.call(this); } function test(){ var dog = new Dog(); dog.sleep("dog"); } </script>
結果是 dog sleep!!。
Animal.call(this)就使得,用Animal對象代替Dog中的this對象,這樣Dog也就繼承了Animal中的所有屬性和方法了。這就實現了javascript里的繼承。
<script> function Animal() { this.name = 'Animal'; this.eat = function() { alert(this.name + " eat!!!"); } } function Dog(){ this.name = 'Dog'; } function test(){ var animal = new Animal(); var dog = new Dog(); animal.eat(); animal.eat.call(dog); } </script>
結果是Animal eat!!!! 和dog eat!!! 這就是常規的用法了。讓方法執行在dog的作用域上。this.name就變成了dog。
嗯,很熟悉吧。其實dojo.hitch()也是這么做的。它用的是apply方法。在前面也提到了,apply和call只有參數的區別。
好了,以下就是dojo.hitch的源碼,
dojo.hitch = function(/*Object*/scope, /*Function|String*/method /*,...*/){ 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 };
細節不表,還是很簡單吧,就是使用apply將對應的方法apply到scope上,並傳入相應的參數。
現在在公司做javascript和dojo。從輕量級的jquery到重量級的dojo,深深地體會到dojo的強大。相信后面有很多文章會介紹dojo和javascript的一些高級特性。