這本書講到變數範疇時,這段看不太懂。書不是我的,先記下來。
這個(有錯的!)程式的計算結果為何?
function wrapElements(a) { var result = [] , i ,n; for (i = 0 , n = a.length ; i < n ; i++) { result[i] = function() { return a[i]; }; } return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f = wrapped[0]; f(); // ?程式設計師的用意可能是要它產生10,但它實際上會產生undefined值。
要了解這個例子為何出錯,就要理解繫結(binding)與指定(assignment)的不同。程式執行期間(runtime)進入到一個範疇(scope)時,會為該範疇內的每一個變數繫結(variable binding)都在記憶體中配置一個「位置」(slot)。warpElements函式繫結了三個區域變數:result、i以及n。所以它被呼叫時,wrapElements會為這三個變數配置位置。在這個迴圈的每次迭代動做(iteration)中,迴圈主體會為其中所嵌套的那個函式(nested function,或稱「巢狀函式」)配置一個closure。
這個程式的臭蟲來源是,程式設計師預期那些函式會在那個巢狀函式被建立之時,儲存當時的i的值,但實際上,它所儲存的是對i的一個參考(reference)。既然i的值也會在每個函式被建立之後改變,那些內層函式所看見的會是i的最終值。這也是closures(閉包)的關鍵所在:
Closure所儲存的,是對它們外層變數的參考(reference),而非那些變數的值(value)。所以wrapElements建立的所有closures都會參考到配置給i的那個位置(slot)。既然迴圈的每次迭代都會遞增i的值,直到i超出陣列索引的範圍為止,那麼我們實際呼叫其中一個closure之時,它在陣列上所查到的索引5就會回傳undefined。
注意到即使我們把var宣告放到for迴圈的標頭(head)中,wrapElements的行為還是完全一樣:
function wrapElements(a) { var result = []; for (var i = 0, n = a.length ; i < n ; i++ ) { result[i] = function() { return a[i]; }; } return result; } var wrapped = wrapElements([10, 20, 30, 40, 50]); var f = wrapped[0]; f(); // undefined這個版本看起來更會騙人,因為var宣告看起來好像是出現在迴圈中,但一如以往,變數的宣告會被拉升(hoist)到外圍函式的頂端,所以再一次地,配置給變數i的只有一個位置(slot)。
解決的方法是建立一個內嵌的巢狀函式以強制創建一個區域範疇(local scope),並且立即呼叫它:
function wrapElements(a) { var result = []; for (var i = 0, n = a.length ; i < n ; i++) { (function(){ var j = i; result[i] = function() { return a[j]; }; })(); } return result; }這個技巧,被稱作是即刻調用的函式運算式(immediately invoked function expression,或IIFE,唸做"iffy"),是JavaScript缺乏區塊範疇的一個不可或缺的變通方法。另一個稍微不同的替代方案是把那個區域變數繫結為IIFE的一個參數,然後把它的值當作引數傳入:
function wrapElements(a) { var result = []; for (var i = 0, n = a.length ; i < n ; i++) { (function(j){ result[i] = function() { return a[j]; }; })(i); } return result; }然而,使用一個IIFE來建立一個區域範疇要小心,因為將一個區塊(block)包在一個函式中,可能會使該區快發生一些細微的改變。首先,這種區塊不能含有任何會跳到區塊外的break或continue述句。因為break或continue到一個函式外是不合法的。第二,如果這種區塊有參考到this或特殊的arguments變數,IIFE會改變他們的意義。
沒有留言:
張貼留言