JavaScript 閉包入門(譯文)

前言

總括 :這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請自行忽略。javascript

譯者 :文章寫在2006年,可直到翻譯的21小時以前做者還在完善這篇文章,在Stackoverflow的How do JavaScript closures work?這個問題裏更是獲得了4000+的贊同,文章內容質量天然沒必要多說。java

本文屬於譯文 git

正文

閉包並非魔法

這篇文章使用有效的javascript代碼向程序員們解釋了閉包,大牛和功能型程序員請自行忽略。程序員

實際上一旦你對閉包的核心概念心照不宣了,閉包就不難理解了,但若是你想經過讀那些學術性文章或是學院派的論文來理解閉包那基本是不可能的。github

本文主要是面向那些有主流程序語言開發經驗或是能看懂下面這段代碼的程序員:web

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');複製代碼

一個閉包小案例

兩種方式歸納:編程

  • 閉包是javascript支持頭等函數的一種方式,它是一個可以引用其內部做用域變量(在本做用域第一次聲明的變量)的表達式,這個表達式能夠賦值給某個變量,能夠做爲參數傳遞給函數,也能夠做爲一個函數返回值返回。

或是數組

  • 閉包是函數開始執行的時候被分配的一個棧幀,在函數執行結束返回後仍不會被釋放(就好像一個棧幀被分配在堆裏而不是棧裏!)

下面這段代碼返回了一個指向這個函數的引用:閉包

function sayHello2(name) {
  var text = 'Hello ' + name; // 局部變量text
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // 打印日誌: "Hello Bob"複製代碼

絕大部分Javascript程序員可以理解上面代碼中的一個函數引用是如何返回賦值給變量say2的,若是你不理解,那麼你須要理解以後再來學習閉包。C語言程序員會認爲這個函數返回一個指向某函數的指針,變量saysay2都是指向某個函數的指針。編程語言

Javascript的函數引用和C語言指針相比還有一個關鍵性的不一樣之處,在Javascript中,一個引用函數的變量能夠看作是有兩個指針,一個是指向函數的指針,一個是指向閉包的隱藏指針。

上面代碼中就有一個閉包,爲何呢?由於匿名函數function() { console.log(text); }是在另外一個函數(在本例中就是sayHello2()函數)聲明的。在Javascript中,若是你在另外一個函數中使用了function關鍵字,那麼你就建立了一個閉包。

在C語言和大多數經常使用程序語言中,當一個函數返回後,函數內聲明的局部變量就不能再被訪問了,由於該函數對應的棧幀已經被銷燬了。

在Javscript中,若是你在一個函數中聲明瞭另外一個函數,那麼在你調用這個函數返回后里面的局部變量仍然是能夠訪問的。這個已經在上面的代碼中演示過了,即咱們在函數sayHello()返回後仍然能夠調用函數say2()注意:咱們在代碼中引用的變量text是咱們在函數sayHello2()中聲明的局部變量。

function() { console.log(text); } // 輸出say2.toString();複製代碼

觀察say2.toString()的輸出,咱們能夠看到確實引用了text變量。匿名函數之因此能夠引用包含'Hello Bob'text變量就是由於sayhello2()的局部變量被保存在了閉包中。

神奇的是,在JavaScript中,函數引用還有一個對於它所建立的閉包的祕密引用,相似於事件委託是一個方法指針加上對於某個對象的祕密引用。

更多例子

出於某種不得而知的緣由,當你去閱讀一些關於閉包的文章的時候,閉包看起來真的是難以理解的。但若是你看到一些你可以去操做的閉包小案例(這花費了我一段時間),閉包就容易理解了。推薦好好推敲下這幾個小案例直到你完全理解了它們究竟是如何工做的。若是你沒徹底弄明白閉包是如何工做的就去盲目使用閉包,會搞出不少神奇的bug的!

例3

局部變量雖然沒有被複制,但能夠經過被引用而被保留下來。這就好像外部函數退出後,但棧幀依舊保存在內存中同樣。

function say667() {
  // 局部變量num最後會保存在閉包中
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // 輸出 43複製代碼

例4

下面三個全局函數對同一個閉包有一個共同的引用,由於他們都是在調用函數setupSomeGlobals()時聲明的。

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // 局部變量num最後會保存在閉包中
  var num = 42;
  // 將一些對於函數的引用存儲爲全局變量
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5
var oldLog = gLogNumber;
setupSomeGlobals();
gLogNumber(); // 42
oldLog() // 5複製代碼

這三個函數具備對同一個閉包的共享訪問權限——這個閉包是指當三個函數定義時setupSomeGlobals()的局部變量。

注意:在上述示例中,當你再次調用setupSomeGlobals()時,一個新的閉包(棧幀)就被建立了。舊變量gLogNumber, gIncreaseNumber, gSetNumber 被有新閉包的函數覆蓋(在JavaScript中,若是你在一個函數中聲明瞭一個新的函數,那麼當外部函數被調用時,內部函數會被從新建立)。

例5

這個示例對於不少人來講都是一個挑戰,因此但願你能弄懂它。注意:當你在一個循環裏面定義一個函數的時候,閉包裏的局部變量可能不會像你想的那樣。

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}
function testList() {
    var fnlist = buildList([1,2,3]);
    // 使用j是爲了防止搞混---可使用i
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}
 testList() //輸出 "item2 undefined" 3 次複製代碼

result.push( function() {console.log(item + ' ' + list[i])}這一行給result數組添加了三次函數匿名引用。若是你不熟悉匿名函數能夠想象成下面代碼:

pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);複製代碼

注意,當你運行上述代碼的時候會打印"item2 undefined"三次!和前面的示例同樣,和buildList的局部變量對應的閉包只有一個。當匿名函數在fnlist[j]()這一行調用的時候,他們使用同一個閉包,並且是使用的這個閉包裏iitem如今的值(循環結束後i的值爲3,item的值爲'item2')。注意:咱們從索引0開始,因此item最後的值爲item2'i的值會被i++增長到3

例6

這個例子代表了閉包會保存函數退出以前內部定義的全部的局部變量。注意:變量alice是在匿名函數以前建立的。 匿名函數先被聲明,而後當它被調用的時候之因此可以訪問alice是由於他們在同一個做用域內(JavaScript作了變量提高),sayAlice()()直接調用了從sayAlice()中返回的函數引用——這個和前面的徹底同樣,只是少了臨時的變量【譯者注:存儲sayAlice()返回的函數引用的變量】

function sayAlice() {
    var say = function() { console.log(alice); }
    // 局部變量最後保存在閉包中
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// 輸出"Hello Alice"複製代碼

技巧:須要注意變量say也是在閉包內部,也能被在sayAlice()內部聲明的其它函數訪問,或者也能夠在函數內部遞歸訪問它。

例7

最後一個例子說明了每次調用函數都會爲局部變量建立一個閉包。實際上每次函數聲明並不會建立一個單獨的閉包,但每次調用函數都會建立一個獨立的閉包。

function newClosure(someNum, someRef) {
    // 局部變量最終保存在閉包中
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '\nanArray ' + anArray.toString() +
            '\nref.someVar ' + ref.someVar);
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;複製代碼

總結

若是任何不太明白的地方最好的方式就是把玩這幾個例子,去機械地閱讀一些文章遠比去作這些實例可貴多。我關於閉包的說明、棧框體(stack-frame)的說明等等,嚴格理論上講並非徹底正確的——它們只是爲了理解而簡化處理過的。當基礎的概念心照不宣以後,就能夠輕鬆地理解這些細節了。

最終總結

  • 每當你在另外一個函數裏使用了關鍵字function,一個閉包就被建立了
  • 每當你在一個函數內部使用了eval(),一個閉包就被建立了。在eval內部你能夠引用外部函數定義的局部變量,一樣的,在eval內部也能夠經過eval('var foo = …')來建立新的局部變量。
  • 當你在一個函數內部使用new function(...)(即構造函數)時,它不會建立閉包(新函數不能引用外部函數的局部變量)。
  • JavaScript中的閉包,就像一個副本,將某函數在退出時候的全部局部變量複製保存其中。
  • 也許最好的理解是閉包老是在進入某個函數的時候被建立,而局部變量是被加入到這個閉包中。
  • 閉包函數每次被調用的時候都會建立一組新的局部變量存儲。(前提是這個函數包含一個內部的函數聲明,而且這個函數的引用被返回或者用某種方法被存儲到一個外部的引用中)
  • 兩個函數或許從源代碼文本上看起來同樣,但由於隱藏閉包的存在會讓兩個函數具備不一樣的行爲。我認爲Javascript代碼實際上並不能找出一個函數引用是否有閉包。
  • 若是你正嘗試作一些動態源代碼的修改(例如:myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));),若是myFunction是一個閉包的話,那麼這並不會生效(固然,你甚至可能歷來都沒有在運行的時候考慮過修改源代碼字符串,可是。。。)。
  • 在函數內部的函數的內部聲明函數是能夠的——能夠得到不止一個層級的閉包。
  • 一般我認爲閉包是一個同時包含函數和被捕捉的變量的術語,可是請注意我並無在本文中使用這個定義。
  • 我以爲JavaScript中的閉包跟其它函數式編程語言中的閉包是有不一樣之處的。

感謝

若是你正好在學習閉包(在這裏或是其餘地方),期待您對本文的任何反饋,您的任何建議均可能會使本文更加清晰易懂。請聯繫jztan1996@gmail.com 【譯者注:這是譯者的郵箱,歡迎交流學習】

後記

這是譯者翻譯的第一篇文章,收穫良多,感受上並不比本身寫一篇文章省事,相反熟悉內容瞭解代碼的同時還得去揣摩做者表達的意圖,難度的確要比本身單獨寫一篇高。能力有限,水平通常,有翻譯不到位的地方,歡迎批評指正。感謝!

相關文章
相關標籤/搜索