討論完理論部分,接下來讓我們來介紹下ECMAScript中閉包究竟是如何實現的。這裡還是有必要再次強調下:ECMAScript只使用靜態(詞法)作用域(而諸如Perl這樣的語言,既可以使用靜態作用域也可以使用動態作用域進行變數聲明)。
var x = 10; function foo() { alert(x); } (function (funArg) { var x = 20; // 变量"x"在(lexical)上下文中静态保存的,在该函数创建的时候就保存了 funArg(); // 10, 而不是20 })(foo);
技術上說,創建該函數的父級上下文的資料是保存在函數的內部屬性 [[Scope]]中的。如果你還不了解什麼是[[Scope]],建議你先閱讀前面的章節,該章節對[[Scope]]作了非常詳細的介紹。如果你對[[Scope]]和作用域鏈的知識完全理解了的話,那對閉包也就完全理解了。
根據函數創建的演算法,我們看到在ECMAScript中,所有的函數都是閉包,因為它們都是在創建的時候就保存了上層上下文的作用域鏈(除開異常的情況) (不管這個函數後續是否會啟動- [[Scope]]在函數創建的時候就有了):
var x = 10; function foo() { alert(x); } // foo是闭包 foo: <FunctionObject> = { [[Call]]: <code block of foo>, [[Scope]]: [ global: { x: 10 } ], ... // 其它属性 };
如我們所說,為了優化目的,當一個函數沒有使用自由變數的話,實作可能不會保存在副作用域鏈裡。不過,在ECMA-262-3規範裡任何都沒說。因此,正常來說,所有的參數都是在創建階段保存在[[Scope]]屬性裡的。
有些實作中,允許對閉包作用域直接進行存取。例如Rhino,針對函數的[[Scope]]屬性,對應有一個非標準的__parent__屬性:
var global = this; var x = 10; var foo = (function () { var y = 20; return function () { alert(y); }; })(); foo(); // 20 alert(foo.__parent__.y); // 20 foo.__parent__.y = 30; foo(); // 30 // 可以通过作用域链移动到顶部 alert(foo.__parent__.__parent__ === global); // true alert(foo.__parent__.__parent__.x); // 10
所有物件都引用一個[[Scope]]
這裡還要注意的是:在ECMAScript中,同一個父上下文中建立的閉包是共用一個[[Scope]]屬性的。也就是說,某個閉包對其中[[Scope]]的變數做修改會影響到其他閉包對其變數的讀取:
var firstClosure; var secondClosure; function foo() { var x = 1; firstClosure = function () { return ++x; }; secondClosure = function () { return --x; }; x = 2; // 影响 AO["x"], 在2个闭包公有的[[Scope]]中 alert(firstClosure()); // 3, 通过第一个闭包的[[Scope]] } foo(); alert(firstClosure()); // 4 alert(secondClosure()); // 3
這就是說:所有的內部函數都共享同一個父作用域
關於這個功能有一個非常普遍的錯誤認識,開發人員在循環語句裡創建函數(內部進行計數)的時候經常得不到預期的結果,而期望是每個函數都有自己的值。
var data = []; for (var k = 0; k < 3; k++) { data[k] = function () { alert(k); }; } data[0](); // 3, 而不是0 data[1](); // 3, 而不是1 data[2](); // 3, 而不是2
上述例子證明了 —— 同一個上下文中創建的閉包是共用一個[[Scope]]屬性的。因此上層上下文中的變數「k」是可以很容易就被改變的。
activeContext.Scope = [ ... // 其它变量对象 {data: [...], k: 3} // 活动对象 ]; data[0].[[Scope]] === Scope; data[1].[[Scope]] === Scope; data[2].[[Scope]] === Scope;
這樣一來,在函數啟動的時候,最後使用到的k就已經變成了3了。如下所示,建立一個閉包就可以解決這個問題了:
var data = []; for (var k = 0; k < 3; k++) { data[k] = (function _helper(x) { return function () { alert(x); }; })(k); // 传入"k"值 } // 现在结果是正确的了 data[0](); // 0 data[1](); // 1 data[2](); // 2
讓我們來看看上述程式碼都發生了什麼事?函數「_helper」創建出來之後,透過傳入參數「k」啟動。其傳回值也是函數,該函數保存在對應的陣列元素中。這種技術產生瞭如下效果: 在函數啟動時,每次“_helper”都會建立一個新的變數對象,其中含有參數“x”,“x”的值就是傳遞進來的“k”的值。這樣一來,返回的函數的[[Scope]]就成瞭如下所示:
data[0].[[Scope]] === [ ... // 其它变量对象 父级上下文中的活动对象AO: {data: [...], k: 3}, _helper上下文中的活动对象AO: {x: 0} ]; data[1].[[Scope]] === [ ... // 其它变量对象 父级上下文中的活动对象AO: {data: [...], k: 3}, _helper上下文中的活动对象AO: {x: 1} ]; data[2].[[Scope]] === [ ... // 其它变量对象 父级上下文中的活动对象AO: {data: [...], k: 3}, _helper上下文中的活动对象AO: {x: 2} ];
我們看到,這時函數的[[Scope]]屬性就有了真正想要的值了,為了達到這樣的目的,我們不得不在[[Scope]]中建立額外的變數物件。要注意的是,在傳回的函數中,如果要取得「k」的值,那麼該值還是會是3。
順便提下,大量介紹JavaScript的文章都認為只有額外創建的函數才是閉包,這種說法是錯誤的。實踐得出,這種方式是最有效的,然而,從理論角度來說,在ECMAScript中所有的函數都是閉包。
然而,上述提到的方法並不是唯一的方法。透過其他方式也可以獲得正確的「k」的值,如下所示:
var data = []; for (var k = 0; k < 3; k++) { (data[k] = function () { alert(arguments.callee.x); }).x = k; // 将k作为函数的一个属性 } // 结果也是对的 data[0](); // 0 data[1](); // 1 data[2](); // 2
Funarg和return
另外一個特性是從閉包中傳回。在ECMAScript中,閉包中的回傳語句會將控制流傳回給呼叫上下文(呼叫者)。而在其他語言中,例如,Ruby,有很多中形式的閉包,相應的處理閉包返回也都不同,下面幾種方式都是可能的:可能直接返回給調用者,或者在某些情況下——直接從上下文退出。
ECMAScript標準的退出行為如下:
function getElement() { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // 返回给函数"forEach"函数 // 而不是返回给getElement函数 alert('found: ' + element); // found: 2 return element; } }); return null; }
然而,在ECMAScript中透過try catch可以實現如下效果:
var $break = {}; function getElement() { try { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // // 从getElement中"返回" alert('found: ' + element); // found: 2 $break.data = element; throw $break; } }); } catch (e) { if (e == $break) { return $break.data; } } return null; } alert(getElement()); // 2
理論版本
這裡說明一下,開發人員經常錯誤將閉包簡化理解成從父上下文版本
🎜這裡說明一下,開發人員經常錯誤將閉包簡化理解成從父上下文版本返回內部函數,甚至理解成只有匿名函數才能是閉包。 🎜再說一下,因為作用域鏈,使得所有的函數都是閉包(與函數型別無關: 匿名函數,FE,NFE,FD都是閉包)。
這裡只有一類函數除外,那就是透過Function建構器所建立的函數,因為其[[Scope]]只包含全域物件。為了更好的澄清問題,我們對ECMAScript中的閉包給出2個正確的版本定義:
ECMAScript中,閉包指的是:
從理論角度:所有的函數。因為它們都在創建的時候就將上層上下文的資料保存起來了。即使是簡單的全域變數也是如此,因為函數中存取全域變數就等於是在存取自由變量,這個時候使用最外層的作用域。
從實踐角度:以下函數才算是閉包:
即使創建它的上下文已經銷毀,它仍然存在(例如,內部函數從父函數中返回)
在程式碼中引用了自由變數
以上就是JavaScript閉包其二:閉包的實現的內容,更多相關內容請關注PHP中文網(www.php.cn)!