iT邦幫忙

7

Javascript中的傳遞參考與closure (2)

I.傳值與傳參考
II. 傳參考與closure (1) --> now
II. 傳參考與closure (2)
II. 傳參考與closure (3)

當我發現我今天必須要寫 closure 的時候,我覺得我瘋了,真的是沒事給自己找事做...
由於 javascript 並不是我第一個學習的程式語言,我相信對很多人來說都是這樣,在其他的語言中,例如 C/C++,甚至是 PHP,對於函式的執行大約是維持了 stack 的結構,每次呼叫函式,就將即將要被執行的函式 push 進去 stack 中,函式中還有再呼叫函式的,就再 push 進去,當正在執行中的這個函式結束或是 return 的時候,就會 pop 出去,將回傳值傳到目前 stack 最上面的那個函式中,利用這個 stack,我們就可以用來記憶目前函式呼叫的順序與層級。

如上圖所示,main函式呼叫了fun1,func1 呼叫了 func2,當 func2 執行完畢後,就會從這個 stack 中 pop 出去,整塊記憶體清得乾乾淨淨,這時候 func1 如果又接著呼叫了 func3,func3 就會再 push 進去,蓋掉那塊記憶體了。由此可知,當執行結束的那個函式被pop掉時,當初在那個函式中的所有區域變數也都不見了。

但是,在 javascript 中,事情好像沒有這麼單純,於是李組長眉頭一皺的看了這個範例:

function foo(){
  var temp = 'azole';
  function bar(){
    console.log(temp);
  }
  return bar;
}
var myFunc = foo();
myFunc(); // azole

咦?咦?咦?當 foo 被呼叫時,建立了一個 temp 區域變數,然後回傳了 bar 這個函式給 myFunc,再然後 foo 就結束了,而作為 foo 的區域變數的 temp,應該就要跟著死掉了,為什麼當我們執行 myFunc 時,還能正確的 log 出 azole 呢?好見鬼阿!

這個就是傳說中的 javascript 的 closure 了,一般對 closure 的解釋大約是內層函式可以取用外層函式定義的變數,或者是可以取用所處環境的變數,這邊我強調是“一般”的說法,因為,單純的使用“取用”或“存取”這兩個字,是無法完全理解 closure 的。

在繼續討論之前,我們先來看一個例子,這是一個很經典的、大家都踩過的 closure 的雷:

for(var i=0; i<10;i++){
  setTimeout(function(){
    console.log(i);	
  }, 500);
}

這裡頭的console並不如預期中的寫出 0, 1, ..., 9,而且寫出了 10 個 10,為什麼為什麼為什麼呢?不是說內層可以取用外層變數嗎?為什麼會是 10 ?

正因為以上這個範例(及其類似的範例),我這個傻人對"一般"的定義一直非常的疑惑與不解,一直到我開始看了 Javascript: The Good Parts 與 Effective Javascript 這兩本書之後,才開始有了一點點理解(也因此才有這篇文章)。

Javascript: The Good Parts 中舉的範例如下:

var quo = function(status){
  return {
    get_status: function(){
      return status;
    }
  };
};

var aQuo = quo('azole');
console.log(aQuo.get_status());

文中的解釋是:

get_status對參數複本(a copy to parameter)沒有存取權,而是對參數本身有存取權。都是因為函式能取用建造它本身的背景情境,才有這種可能。這種狀況稱為 closure(閉包)。

來源 Javascript: The Good Parts 中文版 第 39 頁

我必須說,Javascript: The Good Parts 這本真的是超級好書,要學 javascript 的人一定要讀讀,這本書精簡、有力、字字珠璣,但真的太字字珠璣了,我到這邊還沒有真的看懂,秉持着「人一能之,己百之。人十能之,己千之。果能此道矣,雖愚必明,雖柔必強。」的精神,我多看了十遍,但還是不是真的很理解,有點似懂非懂,還好這時候,還有另外一本書 Effective Javascript 拯救了我。

Effective Javascript 中是這樣說的:

  1. Javascript 允許你**參考(refer to)**定義在目前函式外的變數。
  2. 外層函數回傳(return)之後,內層函式也能參考定義在那些外層函式中的變數。
  3. 它們也能更新外層變數的值。

看到這邊,有沒有發現!?最重要的關鍵字是什麼,就是「參考」這兩個字阿,這時候 Effective Javascript 怕大家還不夠明白,再補上殺手級的一句:Closures實際上儲存的是對那些外層變數的參考(references)。

到這裡,看到參考二字,才真正的明白 Javascript: The Good Parts 中所強調的,對參數複本沒有存取權(這個參數複本指的應該就是值),而是對參數本身(也就是藉由儲存這個參數的參考來去參考到這個參數本身)有存取權。

到此,如果你有看懂,我為何要反覆強調「參考」二字的話,你應該就會知道為什麼那個錯誤的範例總是印出 10,而不是印出我門想要的 0, 1, ..., 9 了,如果不明白也沒關係,明天會講解。(我快沒梗了,給我留一點明天寫。)


尚未有邦友留言

立即登入留言