2014年10月1日 星期三

[JavaScript] 使用即刻調用的函式運算式來建立區域範疇

《Effective JavaScript中文版》-David Herman。
這本書講到變數範疇時,這段看不太懂。書不是我的,先記下來。

這個(有錯的!)程式的計算結果為何?
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會改變他們的意義。

沒有留言:

張貼留言