2014年9月30日 星期二

[JavaScript] Closures 閉包 -2

另外這本書對於Closures也寫得相當詳細,《Effective JavaScript中文版》-David Herman。

Closures(閉包)的概念對之前使用沒有支援這個功能的語言的程式設計師來說,可能很陌生,而初次見到它們時可能還會令人生畏。但請寬心吧,花費功夫去熟悉closures,絕對能夠回本還能到賺好幾倍。

幸運的是,真的沒有什麼好怕的。要了解closures只需要學習三個基本的事實。

第一個就是JavaScript允許你參考(refer to)定義在目前函式外的變數:
function makeSandwich(){
  var magicIngredient = "peanut butter";
  function make(filling){
    returnmagicIngredient + " and " + filling;
  }
  return make("jelly");
}
makeSandwich(); // "peanut butter and jelly"
注意到內層的make函式參考到magicIngredient,一個定義在外層makeSandwich函式的變數。

第二個事實是,即使是在外層函式回傳(return)之後,內層函式也能夠參考定義在那些外層函式中的變數!如果這聽起來不太合理,別忘記JavaScript的函式是第一級的物件(first-class objects)。這意味著你能夠回傳一個內層函式,以供之後呼叫:
function sandwichMaker(){
  var magicIngredient = "peanut butter";
  function make(filling){
    return magicIngredient + " and " + filling;
  }
  return make;
}
var f = sandwichMaker();
f("jelly");  // "peanut butter and jelly"
f("bananas");  // "peanut butter and bananas"
f("marshmallows"); // "peanut butter and marshmallows"
這與第一個範例幾乎完全相同,只不過它不是在外層函式中即刻呼叫make("jelly"),sandwichMaker回傳make函式本身。所以f的值是內層的make函式,而呼叫f等同於呼叫make。然而,即使sandwichMaker已經回傳了,make還是會記得magicIngredient的值。

這是如何運作的呢?答案就是,JavaScript函式值(function values)所包含的資訊,不僅有它們被呼叫時要被執行的程式碼,它們也會在內部儲存任何它們會參考到、定義在包圍它們的範疇(enclosing scopes)中的變數。如果一個函式會追蹤來自包含它們的範疇中的變數,就被稱作是closures(閉包)。這裡的make函式是一個closure,它的程式碼會參考兩個外層變數:maigIngredient及filling。每當make函式被呼叫,它的程式碼就能參考到這兩個變數,因為它們已經被儲存在這個closure之中。

一個函式能夠參考到它範疇中的任何變數,包括參數(parameters)以及外層函式(outer functions)的變數。我們能夠藉此來製作一個更通用的sandwichMaker:
function sandwichMaker(magicIngredient){
  function make(filling){
    return magicIngredient + " and " + filling;
  }
  return make;
}
var hamAnd = sandwichMaker("ham");
hamAnd("cheese"); // "ham and cheese"
hamAnd("mustard"); // "ham and mustard"
var turkeyAnd = sandwichMaker("turkey");
turkeyAnd("Swiss"); // "turkey and Swiss"
trukeyAnd("Provolone"); // "turkey and Provolone"
這個範例建立了兩個不同的函式,hamAnd和turkeyAnd。即使它們兩者皆來自相同的make定義,它們仍是兩個不同的物件:第一個函式儲存"ham"作為magicIngredient的值,而另一個則儲存"turkey"。

Closures是JavaScript最優雅也最具有表達能力的功能之一,並且是許多慣用語法(idioms)的核心。JavaScript甚至提供一種更便利的字面值與法(literal syntax)來建構closures,也就是函式運算式(function expression):
function sandwichMaker(magicIngredient){
  return function(filling){
    return magicIngredient + " and " + filling;
  };
}
注意到這個函式運算式是匿名的(anonymous):既然我們只是要用它來產生一個新的函式值,而非想要在區域範疇內呼叫它,我們就不需要為那個函式命名。不過函式運算式還是能夠有名稱。

有關closures,第三個也是最後一個要知道的事實是,它們能夠更新外層變數(outer variables)的值。Closures實際上儲存的是對那些外層變數的參考(references),而非複製它們的值,因此對它們的更新動作,任何能夠存取它們的closures都看得到。一個能夠演示這個事實的慣用語法是一個box:它是一個物件,會在內部儲存可被讀取或更新的值:
function box(){
  var val = undefined;
  return {
    set: function(newVal) { val = newVal; },
    get: function() { return val; },
    type: function() { return typeof val; }
  };
}
var b = box();
b.type();	// "undefined"
b.set(98.6);
b.get();	// 98.6
b.type();	// "number"
此範例會產生一個物件,它含有三個closures:它的set、get與type特性。這三個closures都能夠存取那個val變數。set這個closure會更新val的值,而更新後呼叫get與type就能看到這個更新動作的結果。

沒有留言:

張貼留言