JavaScript閉包如何工作?

How do JavaScript closures work?
投票:7645回答:86

您將如何向了解了閉包本身的概念(例如函數,變量等)的人解釋JavaScript閉包,但卻不了解閉包本身?

我已經在Wikipedia上看到了Scheme示例 ,但是不幸的是它沒有幫助。

tags:javascript,function,variables,scope,closures
86回答
87
投票

我知道已經有很多解決方案,但是我猜想這個小而簡單的腳本可以用來說明這個概念:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined

7065
投票

面向初學者的JavaScript關閉

莫里斯在2006年2月2日星期二提交。 從此開始由社區編輯。

關閉不是魔術

本頁說明了閉包,以便程序員可以使用有效的JavaScript代碼來理解閉包。 它不適用於專家或功能性程序員。

一旦核心概念浮出水面,關閉就不難理解。 但是,通過閱讀任何理論或學術上的解釋是不可能理解它們的!

本文面向具有某種主流語言編程經驗並且可以閱讀以下JavaScript函數的程序員:

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

兩篇摘要

  • 當一個函數( foo )聲明其他函數(bar和baz)時,在該函數退出時,在foo創建的局部變量族不會被破壞 這些變量只是對外界不可見。 因此, foo可以巧妙地返回功能barbaz ,並且它們可以通過這個封閉的變量家族(“封閉”)繼續進行讀取,寫入和通信,這些變量是其他任何人都無法干預的,甚至沒有人可以介入foo再來一次。

  • 閉包是支持一流功能的一種方式。 它是一個表達式,可以引用其範圍內的變量(首次聲明時),分配給變量,作為參數傳遞給函數或作為函數結果返回。

閉包的例子

以下代碼返回對函數的引用:

 function sayHello2(name) { var text = 'Hello ' + name; // Local variable var say = function() { console.log(text); } return say; } var say2 = sayHello2('Bob'); say2(); // logs "Hello Bob" 

大多數JavaScript程序員將理解上面代碼中如何將對函數的引用返回到變量( say2 )。 如果不這樣做,那麼您需要先研究一下閉包。 使用C的程序員會將函數視為返回函數的指針,並且變量saysay2分別是函數的指針。

指向函數的C指針和指向函數的JavaScript引用之間存在關鍵區別。 在JavaScript中,你可以把一個函數引用變量作為既具有指針功能以及一個隱藏的指針關閉。

上面的代碼已關閉,因為匿名函數function() { console.log(text); } function() { console.log(text); } 另一個函數內部聲明,在此示例中為sayHello2() 在JavaScript中,如果在另一個函數中使用function關鍵字,則將創建一個閉包。

在C和其他大多數常用語言,函數返回 ,因為堆棧幀被摧毀了所有的局部變量都不再使用。

在JavaScript中,如果在另一個函數中聲明一個函數,則外部函數從其返回後仍可訪問。 上面已經演示了這一點,因為我們從sayHello2()返回後調用了函數say2() sayHello2() 請注意,我們調用的代碼引用了變量text ,這是函數sayHello2()局部變量

function() { console.log(text); } // Output of say2.toString();

查看say2.toString()的輸出,我們可以看到該代碼引用了變量text 匿名函數可以引用包含值'Hello Bob' text ,因為sayHello2()的局部變量已在閉包中秘密保持活動狀態。

天才之處在於,在JavaScript中,函數引用還具有對其創建於其內的閉包的秘密引用—類似於委託如何成為方法指針以及對對象的秘密引用。

更多例子

由於某些原因,當您閱讀閉包時,似乎真的很難理解它,但是當您看到一些示例時,很清楚它們是如何工作的(花了我一段時間)。 我建議仔細研究這些示例,直到您理解它們的工作原理。 如果您在不完全了解閉包工作原理的情況下開始使用閉包,那麼您很快就會創建一些非常奇怪的錯誤!

例子3

此示例顯示局部變量未復制-通過引用保留它們。 似乎即使外部函數退出後,堆棧框架仍在內存中保持活動狀態!

 function say667() { // Local variable that ends up within closure var num = 42; var say = function() { console.log(num); } num++; return say; } var sayNumber = say667(); sayNumber(); // logs 43 

例子4

這三個全局函數都對同一閉包有共同的引用,因為它們都在一次調用setupSomeGlobals()

 var gLogNumber, gIncreaseNumber, gSetNumber; function setupSomeGlobals() { // Local variable that ends up within closure var num = 42; // Store some references to functions as global variables 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() ,則會創建一個新的閉包(堆棧框架!)。 舊的gLogNumbergIncreaseNumbergSetNumber變量將被具有新閉包的函數覆蓋。 (在JavaScript中,每當在另一個函數中聲明一個函數時,每次調用外部函數時都會重新創建一個 (或多個)內部函數。)

例子5

此示例顯示閉包包含退出前在外部函數內部聲明的任何局部變量。 請注意,變量alice實際上是在匿名函數之後聲明的。 首先聲明匿名函數,並在調用該函數時可以訪問alice變量,因為alice在同一作用域內(JavaScript進行變量提升 )。 同樣, sayAlice()()只是直接調用從sayAlice()返回的函數引用-它與之前所做的操作完全相同,但沒有臨時變量。

 function sayAlice() { var say = function() { console.log(alice); } // Local variable that ends up within closure var alice = 'Hello Alice'; return say; } sayAlice()();// logs "Hello Alice" 

棘手:請注意, say變量也位於閉包內部,可以由在sayAlice()聲明的任何其他函數訪問,或者可以在內部函數中遞歸訪問。

例子6

對於許多人來說,這是一個真正的陷阱,因此您需要了解它。 如果要在循環內定義函數,請非常小心:閉包中的局部變量可能不會像您首先想到的那樣起作用。

您需要了解Javascript中的“變量提升”功能才能了解此示例。

 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]); // Using j only to help prevent confusion -- could use i. for (var j = 0; j < fnlist.length; j++) { fnlist[j](); } } testList() //logs "item2 undefined" 3 times 

result.push( function() {console.log(item + ' ' + list[i])}會在結果數組中添加對匿名函數的引用三遍,如果您不太熟悉匿名函數,請考慮一下就如:

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

請注意,在運行示例時, "item2 undefined"被記錄了三次! 這是因為就像前面的示例一樣, buildList的局部變量( resultilistitem )只有一個閉包。 當在行上調用匿名函數fnlist[j]() 它們都使用相同的單個閉包,並且使用該閉包中iitem的當前值(其中i的值為3因為循環已完成, item的值為'item2' )。 注意,我們從0開始索引,因此item的值為item2 i ++將i遞增到值3

查看使用變量item的塊級聲明(通過let關鍵字)而不是通過var關鍵字進行函數範圍的變量聲明時,會發生什麼。 如果進行了更改,則數組result中的每個匿名函數都有其自己的關閉; 運行示例時,輸出如下:

item0 undefined
item1 undefined
item2 undefined

如果變量i也是使用let而不是var定義的,則輸出為:

item0 1
item1 2
item2 3

例子7

在最後一個示例中,對主函數的每次調用都會創建一個單獨的閉包。

 function newClosure(someNum, someRef) { // Local variables that end up within closure var num = someNum; var anArray = [1,2,3]; var ref = someRef; return function(x) { num += x; anArray.push(num); console.log('num: ' + num + '; anArray: ' + anArray.toString() + '; ref.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; 

摘要

如果一切似乎都不清楚,那麼最好的辦法就是看這些例子。 閱讀說明比理解示例困難得多。 我對閉包和堆棧框架等的解釋在技術上並不正確-它們是旨在幫助理解的粗略簡化。 一旦了解了基本概念,您便可以稍後進行詳細了解。

最後一點:

  • 每當在另一個函數中使用function ,都會使用閉包。
  • 每當在eval()內部使用eval() ,都會使用閉包。 eval的文本可以引用函數的局部變量,並且在eval中甚至可以使用eval('var foo = …')創建新的局部變量eval('var foo = …')
  • 函數內部使用new Function(…)函數構造函數)時,它不會創建閉包。 (新函數不能引用外部函數的局部變量。)
  • JavaScript中的閉包就像保留所有局部變量的副本一樣,就像它們退出函數時一樣。
  • 最好考慮一下,總是總是只在函數的入口處創建一個閉包,並且將局部變量添加到該閉包中。
  • 每次調用帶有閉包的函數時,都會保留一組新的局部變量(假設該函數內部包含函數聲明,並且將返回對該內部函數的引用或以某種方式為其保留外部引用)。
  • 兩個函數可能看起來像具有相同的源文本,但是由於它們的“隱藏”關閉而具有完全不同的行為。 我認為JavaScript代碼實際上無法找出函數引用是否具有閉包。
  • 如果您嘗試進行任何動態源代碼修改(例如: myFunction = Function(myFunction.toString().replace(/Hello/,'Hola')); ),則如果myFunction是閉包(當然,您甚至都不會想到在運行時進行源代碼字符串替換,而是...)。
  • 可以在函數內的函數聲明中獲取函數聲明……並且可以在多個級別上獲得閉包。
  • 我認為通常閉包既是函數又是捕獲的變量的術語。 請注意,我不在本文中使用該定義!
  • 我懷疑JavaScript中的閉包與功能性語言中的閉包不同。

鏈接

謝謝

如果您剛剛學習了閉包(在這里或其他地方!),那麼我對您可能會建議使本文更清晰的任何更改所產生的反饋意見感興趣。 發送電子郵件至morrisjohns.com(morris_closure @)。 請注意,我不是JavaScript專家,也不是閉包專家。


莫里斯(Morris)的原始帖子可以在Internet存檔中找到。


366
投票

閉包很像一個對象。 每當您調用函數時,它都會實例化。

JavaScript中閉包的範圍是詞法的,這意味著該閉包所屬的函數中包含的所有內容都可以訪問其中的任何變量。

如果您在閉包中包含一個變量,

  1. 給它分配var foo=1; 要么
  2. 只需寫var foo;

如果內部函數(包含在另一個函數中的函數)在不使用var定義其範圍的情況下訪問此類變量,則會在外部閉包中修改變量的內容。

閉包的壽命超過了產生它的函數的運行時間。 如果其他函數超出了定義它們的閉包/範圍 (例如,作為返回值),則這些函數將繼續引用該閉包

 function example(closure) { // define somevariable to live in the closure of example var somevariable = 'unchanged'; return { change_to: function(value) { somevariable = value; }, log: function(value) { console.log('somevariable of closure %s is: %s', closure, somevariable); } } } closure_one = example('one'); closure_two = example('two'); closure_one.log(); closure_two.log(); closure_one.change_to('some new value'); closure_one.log(); closure_two.log(); 

輸出量

somevariable of closure one is: unchanged
somevariable of closure two is: unchanged
somevariable of closure one is: some new value
somevariable of closure two is: unchanged

486
投票

閉包很難解釋,因為閉包用於使某些行為正常工作,每個人都希望它們能正常工作。 我發現解釋它們的最佳方法(以及了解它們的方法)是想像沒有它們的情況:

  var bind = function(x) { return function(y) { return x + y; }; } var plus5 = bind(5); console.log(plus5(3)); 

如果JavaScript 知道閉包,在這裡會發生什麼? 只需將最後一行的調用替換為其方法主體(基本上是函數調用所做的工作),您將獲得:

console.log(x + 3);

現在, x的定義在哪裡? 我們沒有在當前範圍內對其進行定義。 唯一的解決方案是讓plus5 攜帶其範圍(或更確切地說,其父級的範圍)。 這樣, x定義明確,並綁定到值5。


3940
投票

每當您在另一個函數中看到function關鍵字時,內部函數就可以訪問外部函數中的變量。

 function foo(x) { var tmp = 3; function bar(y) { console.log(x + y + (++tmp)); // will log 16 } bar(10); } foo(2); 

這將始終記錄16,因為bar可以訪問x它被定義為foo的參數),並且還可以從foo訪問tmp

一個封閉。 一個函數不必為了被稱為閉包而返回 只需訪問直接詞法範圍之外的變量即可創建閉包

 function foo(x) { var tmp = 3; return function (y) { console.log(x + y + (++tmp)); // will also log 16 } } var bar = foo(2); // bar is now a closure. bar(10); 

上面的函數也會記錄16,因為bar仍然可以引用xtmp ,即使它不再直接在範圍內。

但是,由於tmp仍然在bar的閉包內部徘徊,因此它也在增加。 每次調用bar時,它將增加。

關閉的最簡單示例是:

 var a = 10; function test() { console.log(a); // will output 10 console.log(b); // will output 6 } var b = 6; test(); 

調用JavaScript函數時,將創建一個新的執行上下文。 與函數參數和父對像一起,此執行上下文還接收在其外部聲明的所有變量(在上面的示例中,“ a”和“ b”)。

通過返回一個閉包函數列表或將它們設置為全局變量,可以創建多個閉包函數。 所有這些都將引用相同的 x和相同的tmp ,而不是自己製作副本。

在此,數字x是文字數字。 與JavaScript中的其他文字一樣,當調用foo時,數字x 作為參數x 複製foo中。

另一方面,JavaScript在處理對象時總是使用引用。 如果說,您用一個對象調用了foo ,則它返回的閉包將引用該原始對象!

 function foo(x) { var tmp = 3; return function (y) { console.log(x + y + tmp); x.memb = x.memb ? x.memb + 1 : 1; console.log(x.memb); } } var age = new Number(2); var bar = foo(age); // bar is now a closure referencing age. bar(10); 

不出所料,每次調用bar(10)都會使x.memb遞增。 可能不會想到的是, x只是與age變量引用相同的對象! 在兩次致電barage.memb將為2! 此引用是HTML對象內存洩漏的基礎。


88
投票

閉包是內部函數可以訪問其外部函數中的變量的地方。 這可能是閉包最簡單的單行解釋。


50
投票

JavaScript中的函數不僅是對一組指令的引用(如C語言),而且還包括一個隱藏的數據結構,該結構由對其使用的所有非局部變量(捕獲的變量)的引用組成。 這種兩件式功能稱為閉包。 JavaScript中的每個函數都可以視為閉包。

閉包是具有狀態的功能。 從某種意義上說,它與“ this”類似,因為“ this”還提供了函數的狀態,但function和“ this”是單獨的對象(“ this”只是一個奇特的參數,並且是將其永久綁定到功能是創建一個閉包)。 雖然“ this”和函數始終是分開存在的,但不能將函數與其閉包分開,並且語言不提供訪問捕獲變量的方法。

因為詞彙嵌套函數引用的所有這些外部變量實際上都是其詞彙包圍函數鏈中的局部變量(可以將全局變量假定為某個根函數的局部變量),並且每次執行函數都會創建一個新的實例。隨之而來的是,函數每次執行返回(或以其他方式將其轉移出去,例如將其註冊為回調)嵌套函數時,都會創建一個新的閉包(具有其自身可能唯一的一組引用非局部變量,這些變量代表其執行)上下文)。

另外,必須理解,JavaScript中的局部變量不是在堆棧框架上創建的,而是在堆上創建的,並且僅在沒有人引用它們時才銷毀。 當函數返回時,對其局部變量的引用會遞減,但是如果它們在當前執行期間成為閉包的一部分並且仍由其詞法嵌套的函數引用(它們只有在對這些嵌套函數被返回或以其他方式轉移到某些外部代碼)。

一個例子:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();

572
投票

稻草人

我需要知道一個按鈕被點擊了多少次,並且每三次單擊都會執行某項操作...

相當明顯的解決方案

 // Declare counter outside event handler's scope var counter = 0; var element = document.getElementById('button'); element.addEventListener("click", function() { // Increment outside counter counter++; if (counter === 3) { // Do something every third time console.log("Third time's the charm!"); // Reset counter counter = 0; } }); 
 <button id="button">Click Me!</button> 

現在這將起作用,但是它通過添加變量來侵入外部範圍,該變量的唯一目的是跟踪計數。 在某些情況下,這是可取的,因為您的外部應用程序可能需要訪問此信息。 但是在這種情況下,我們僅更改每三次單擊的行為,因此最好將此功能封裝在事件處理程序中

考慮這個選項

 var element = document.getElementById('button'); element.addEventListener("click", (function() { // init the count to 0 var count = 0; return function(e) { // <- This function becomes the click handler count++; // and will retain access to the above `count` if (count === 3) { // Do something every third time console.log("Third time's the charm!"); //Reset counter count = 0; } }; })()); 
 <button id="button">Click Me!</button> 

注意這裡的幾件事。

在上面的示例中,我正在使用JavaScript的關閉行為。 此行為允許任何函數無限期地訪問其創建範圍。 為了實際應用這一點,我立即調用了一個返回另一個函數的函數,並且由於我返回的函數可以訪問內部count變量(由於上述閉包行為),因此會導致私有範圍供結果使用功能...不是那麼簡單嗎? 讓我們稀釋一下...

簡單的單行閉包

//          _______________________Immediately invoked______________________
//         |                                                                |
//         |        Scope retained for use      ___Returned as the____      |
//         |       only by returned function   |    value of func     |     |
//         |             |            |        |                      |     |
//         v             v            v        v                      v     v
var func = (function() { var a = 'val'; return function() { alert(a); }; })();

返回函數以外的所有變量都可用於返回函數,但不能直接用於返回函數對象...

func();  // Alerts "val"
func.a;  // Undefined

得到它? 因此,在我們的主要示例中,count變量包含在閉包中,並且始終可供事件處理程序使用,因此它在單擊之間保持其狀態。

同樣,此私有變量狀態是完全可訪問的,既可以讀取也可以分配給其私有範圍的變量。

你去了 您現在完全封裝了此行為。

完整的博客文章 (包括jQuery注意事項)


57
投票

好吧,和一個6歲的孩子聊天,我可能會使用以下關聯。

想像一下-您正在與整個房子裡的弟弟和妹妹一起玩耍,並且帶著玩具走來走去,並將其中一些帶入哥哥的房間。 一段時間後,您的兄弟從學校返回並去了他的房間,他鎖在房間裡,所以現在您無法直接訪問留在這裡的玩具了。 但是你可以敲門,問你的兄弟那個玩具。 這稱為玩具的閉合 你的兄弟為你做了補償,現在他已經進入了範圍

與之相比,情況是一扇門被通風裝置鎖住而裡面沒有人(執行常規功能),然後發生局部火災並燒毀了房間(垃圾收集器:D),然後建造了一個新房間,現在您可以離開了那裡有另一個玩具(新功能實例),但是永遠不會得到第一個房間實例中剩下的相同玩具。

對於高齡兒童,我會輸入以下內容。 它不是完美的,但是會讓您感覺到它是什麼:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

如您所見,不管房間是否上鎖,仍然可以通過兄弟訪問留在房間裡的玩具。 這是一個可玩的jsbin


46
投票

我只是將它們指向Mozilla Closures頁面 這是我發現的關於閉包基礎知識和實際用法的最佳,最簡潔明了的解釋 強烈建議任何學習JavaScript的人使用。

是的,我什至推薦給6歲的孩子使用-如果6歲的孩子正在學習閉包,那麼可以隨時理解本文提供的簡潔明了的邏輯是合乎邏輯的。


50
投票

一個六歲孩子的答案(假設他知道什麼是函數,什麼是變量以及什麼數據):

函數可以返回數據。 您可以從函數返回的一種數據是另一種函數。 返回該新函數時,創建該函數的函數中使用的所有變量和參數都不會消失。 而是,該父函數“關閉”。 換句話說,除了返回的功能外,什麼都看不到它,也看不到它使用的變量。 該新函數具有特殊的功能,可以回顧創建它的函數內部並查看其中的數據。

function the_closure() {
  var x = 4;
  return function () {
    return x; // Here, we look back inside the_closure for the value of x
  }
}

var myFn = the_closure();
myFn(); //=> 4

解釋它的另一種非常簡單的方法是在範圍上:

每當您在較大範圍內創建較小範圍時,較小範圍將始終能夠看到較大範圍中的內容。


366
投票

好吧,6歲的瓶蓋粉絲。 您是否想听到最簡單的閉包示例?

讓我們想像下一個情況:駕駛員坐在汽車上。 那輛車在飛機上。 飛機在機場。 駕駛員即使在飛機離開機場後也無法進入汽車外部但在飛機內部的東西,這是封閉的。 而已。 27歲時,請查看更詳細的說明或以下示例。

這是將飛機上的故事轉換為代碼的方法。

 var plane = function(defaultAirport) { var lastAirportLeft = defaultAirport; var car = { driver: { startAccessPlaneInfo: function() { setInterval(function() { console.log("Last airport was " + lastAirportLeft); }, 2000); } } }; car.driver.startAccessPlaneInfo(); return { leaveTheAirport: function(airPortName) { lastAirportLeft = airPortName; } } }("Boryspil International Airport"); plane.leaveTheAirport("John F. Kennedy"); 


171
投票

通過GOOD / BAD比較,我傾向於學得更好。 我喜歡看到有人可能會遇到的工作代碼,然後是非工作代碼。 我整理了一個jsFiddle進行比較,並嘗試將差異歸結為我能想到的最簡單的解釋。

關閉正確:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • 在上面的代碼中,循環的每次迭代都調用createClosure(n) 請注意,我將變量n命名為突出顯示它是在新函數作用域中創建的變量,而不是與綁定到外部作用域的index相同的變量。

  • 這將創建一個新範圍,並且n綁定到該範圍; 這意味著我們有10個單獨的範圍,每個迭代一個。

  • createClosure(n)返回一個函數,該函數返回該範圍內的n。

  • 在每個範圍內, n綁定到調用createClosure(n)時具有的任何值,因此要返回的嵌套函數將始終返回調用createClosure(n)時具有的n的值。

關閉做錯了:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • 在上面的代碼中,循環在createClosureArray()函數內移動,該函數現在僅返回完成的數組,乍一看似乎更直觀。

  • 可能不明顯的是,由於僅調用一次createClosureArray()才能為此函數創建一個作用域,而不是每次循環都創建一個作用域。

  • 在此函數中,定義了一個名為index的變量。 循環運行並將函數添加到返回index的數組中。 請注意, index是在createClosureArray函數中定義的,該函數只能被調用一次。

  • 由於createClosureArray()函數中只有一個作用域,因此index僅綁定到該範圍內的一個值。 換句話說,每次循環更改index的值時,都會為該範圍內引用它的所有內容更改它。

  • 添加到數組中的所有函數都從定義它的父作用域返回SAME index變量,而不是像第一個示例那樣從10個不同作用域中返回10個不同的變量。 最終結果是所有10個函數都從同一作用域返回相同的變量。

  • 循環結束並完成index修改後,最終值為10,因此,添加到數組的每個函數都將返回單個index變量的值,該值現在設置為10。

結果

正確關閉
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

關閉錯誤
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10


216
投票

閉包很簡單:

以下簡單示例涵蓋了JavaScript閉包的所有要點。 *

這是一家生產可以加和乘的計算器的工廠:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

關鍵點:每次對make_calculator調用make_calculator創建一個新的局部變量n ,在make_calculator返回很長時間之後,該局部變量n仍可被該計算器的addmultiply函數使用。

如果您熟悉堆棧框架,這些計算器似乎很奇怪: make_calculator返回後,它們如何繼續訪問n 答案是想像JavaScript不使用“堆棧框架”,而是使用“堆框架”,該堆可以在使它們返回的函數調用之後持續存在。

addmultiply這樣的內部函數被稱為閉包 ,它們訪問在外部函數**中聲明的變量。

這幾乎是閉包的全部內容。



*例如,它涵蓋了另一個答案中 “封閉的傻瓜”一文中的所有要點,例6除外,該示例僅表明變量可以在聲明之前使用,這是一個不錯的事實,但與閉包完全無關。 它還涵蓋了接受的答案所有要點,除了以下幾點:(1)函數將其參數複製到局部變量(命名的函數參數),以及(2)複製數字創建新的數字,但複製對象引用給您另一個對同一對象的引用。 這些也是很好知道的,但又與閉包完全無關。 它也與該答案中的示例非常相似,但更簡短,更抽象。 它沒有涵蓋此答案註釋的要點,這是因為JavaScript使得很難將循環變量的當前值插入到您的內部函數中:“插入”步驟只能使用包含輔助函數的函數來完成您的內部函數,並在每次循環迭代時調用。 (嚴格來說,內部函數訪問變量的幫助函數的副本,而不是插入任何內容。)再次,這在創建閉包時非常有用,但不是閉包的一部分或工作方式的一部分。 由於閉包在ML之類的功能語言中的工作方式不同,因此還有更多的困惑,其中變量綁定到值而不是存儲空間,從而提供了源源不斷的了解閉包的人(即“插入”方式),即對於JavaScript而言,這是完全不正確的,因為JavaScript總是將變量綁定到存儲空間,而不是綁定到值。

**任何外部函數(如果有多個嵌套函數),甚至是在全局上下文中,正如該答案清楚指出的那樣。


205
投票

我如何向六歲的孩子解釋:

您知道大人如何擁有房屋,他們稱之為房屋嗎? 當媽媽有孩子時,孩子實際上並不擁有任何東西,對嗎? 但是它的父母擁有一所房子,因此只要有人問孩子“你的房子在哪裡?”,他/她就可以回答“那所房子!”,並指向其父母的房子。 “關閉”是孩子始終(即使在國外)也能夠說自己擁有房屋的能力,即使這實際上是父母擁有房屋的能力。


365
投票

這是為了消除對其他一些答案中出現的閉包的幾種(可能的)誤解。

  • 閉包不僅在您返回內部函數時創建。 實際上,封閉函數根本不需要返回即可創建封閉函數。 您可以改為將內部函數分配給外部作用域中的變量,或將其作為參數傳遞給另一個函數,在該函數中可以立即或在以後的任何時間調用它。 因此,封閉函數的關閉很可能在調用封閉函數後立即創建因為只要在調用封閉函數之前或之後,任何內部函數都可以訪問該封閉。
  • 閉包在其範圍內未引用變量的舊值的副本。 變量本身是閉包的一部分,因此訪問這些變量之一時看到的值是訪問它時的最新值。 這就是為什麼在循環內部創建內部函數會很棘手的原因,因為每個函數都可以訪問相同的外部變量,而不是在創建或調用函數時獲取變量的副本。
  • 閉包中的“變量”包括在函數內聲明的任何命名函數。 它們還包括函數的參數。 閉包還可以訪問其包含的閉包的變量,直到全局範圍為止。
  • 閉包使用內存,但是它們不會導致內存洩漏,因為JavaScript本身會清理自己的未引用的循環結構。 當Internet Explorer無法斷開引用閉包的DOM屬性值的連接時,會創建涉及閉包的Internet Explorer內存洩漏,從而維護對可能的圓形結構的引用。

60
投票

作為一個六歲的孩子的父親,他目前正在教幼兒(並且是一個相對新手,沒有正規教育的編碼,因此需要更正),我認為這堂課最好通過動手游戲來保持。 如果6歲的孩子準備好了解什麼是封閉,那麼他們已經足夠大了,可以自己去嘗試。 我建議將代碼粘貼到jsfiddle.net中,進行一些解釋,然後讓它們單獨編造一首獨特的歌曲。 下面的解釋性文字可能更適合10歲以下的兒童。

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "\n" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "\n" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "\n" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "\n" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

使用說明

數據:數據是事實的集合。 它可以是數字,單詞,量度,​​觀察值,甚至只是事物的描述。 您不能觸摸,聞到或嚐嚐它。 您可以寫下來,說出來並聽到。 您可以使用它在計算機上產生觸摸氣味和味道。 計算機可以使用代碼使它變得有用。

代碼:上面的所有文字都稱為代碼 它是用JavaScript編寫的。

JAVASCRIPT:JavaScript是一種語言。 像英語或法語或中文都是語言。 計算機和其他電子處理器可以理解許多語言。 為了使JavaScript能夠被計算機理解,它需要一個解釋器。 想像一下,如果一位只會說俄語的老師來學校教您的課程。 當老師說“всесадятся”時,全班聽不懂。 但是幸運的是,您的班上有一個俄羅斯學生,他告訴每個人這意味著“每個人都坐下”-你們都一樣。 全班就像一台電腦,俄語學生是口譯員。 對於JavaScript,最常見的解釋器稱為瀏覽器。

瀏覽器:當您通過計算機,平板電腦或手機上的Internet連接訪問網站時,便使用了瀏覽器。 您可能知道的示例是Internet Explorer,Chrome,Firefox和Safari。 瀏覽器可以理解JavaScript並告訴計算機它需要做什麼。 JavaScript指令稱為函數。

功能:JavaScript中的函數就像一個工廠。 這可能是一個只有一台機器的小工廠。 或者它可能包含其他許多小工廠,每個工廠都有許多從事不同工作的機器。 在現實生活中的服裝工廠中,可能會有成堆的布料和線筒進來,而T恤和牛仔褲就出來了。 我們的JavaScript工廠僅處理數據,無法縫製,鑽孔或熔化金屬。 在我們的JavaScript工廠中,數據進入並且數據出來。

所有這些數據聽起來都有些無聊,但確實非常酷。 我們可能有一個告訴機器人晚餐的功能。 假設我邀請您和您的朋友來我家。 您最喜歡雞腿,我喜歡香腸,您的朋友總是想要您想要的東西,而我的朋友不吃肉。

我沒有時間去購物,因此該功能需要知道我們在冰箱中的存貨才能做出決定。 每種食材的烹飪時間都不一樣,我們希望機器人同時將所有食物加熱。 我們需要向功能提供所需數據,功能可以與冰箱“對話”,並且功能可以控制機器人。

函數通常具有名稱,括號和花括號。 像這樣:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

需要注意的是/*...*///由瀏覽器讀取停止代碼。

NAME:您可以隨便使用任何單詞都可以調用一個函數。 示例“ cookMeal”通常將兩個單詞連接在一起,並在第二個單詞開頭加一個大寫字母-但這不是必需的。 它不能有空格,也不能單獨是數字。

家長:“括號”或()是JavaScript函數工廠門上的信箱或街道上用於向工廠發送信息包的信箱。 有時可能會標記郵箱,例如 cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime) ,在這種情況下,您知道必須提供哪些數據。

大括號:看起來像{} “括號”是我們工廠的有色窗戶。 從工廠內部可以看到,但是從外部看不到。

上面的長代碼示例

我們的代碼以單詞function開頭,因此我們知道它是其中之一! 然後,函數的名稱會唱歌 -這是我自己對函數含義的描述。 然後括號() 括號總是存在於函數中。 有時它們是空的,有時它們中有東西。這個(person)有一個詞: (person) 在這之後有一個這樣的括號{ 這標誌著函數sing()的開始 它有一個標記sing()結束的伙伴,例如}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

因此,此功能可能與唱歌有關,並且可能需要有關某個人的一些數據。 它內部有指令來處理這些數據。

現在,在函數sing()之後 ,代碼的結尾附近是該行

var person="an old lady";

變量:字母var代表“變量”。 變量就像一個信封。 在外面的信封上標有“人”字樣。 它的內部包含一張紙條,上面有我們的功能所需的信息,一些字母和空格像一條繩子(稱為繩子)連接在一起,構成一個短語,上面寫著“一位老太太”。 我們的信封可能包含其他種類的東西,例如數字(稱為整數),指令(稱為函數),列表(稱為數組 )。 因為此變量寫在所有大括號{} ,並且因為當您位於大括號內時可以看到著色窗口,所以可以從代碼的任何位置看到此變量。 我們稱其為“全局變量”。

全局變量: 人員是全局變量,這意味著如果將其值從“老太太”更改為“年輕人”,則該將一直是年輕人,直到您決定再次更改它,並且其他任何功能代碼可以看到這是一個年輕人。 F12按鈕或查看“選項”設置以打開瀏覽器的開發人員控制台,然後鍵入“ person”以查看該值是什麼。 鍵入person="a young man"進行更改,然後再次鍵入“ person”以查看其已更改。

在此之後,我們有線

sing(person);

該行正在調用函數,就像在調用狗一樣

“來 ,來吧,讓的人 !”

當瀏覽器加載JavaScript代碼並到達此行時,它將啟動該功能。 我將這一行放在最後,以確保瀏覽器具有運行它所需的所有信息。

功能定義動作-主要功能是唱歌。 它包含一個名為firstPart的變量,該變量適用於歌唱每首歌的人的歌唱:“有“ +人+誰在吞嚥”。 如果在控制台中鍵入firstPart ,將不會得到答案,因為該變量已鎖定在函數中-瀏覽器無法在花括號的著色窗口內部看到。

關閉:關閉是在sing()函數內部的較小函數。 大工廠裡面的小工廠。 它們每個都有自己的大括號,這意味著它們的變量無法從外部看到。 這就是為什麼變量名( 生物結果 )可以在閉包中重複但值不同的原因。 如果在控制台窗口中鍵入這些變量名,則不會得到它的值,因為它被兩層著色窗口隱藏了。

閉包都知道sing()函數名為firstPart的變量是什麼,因為它們可以從其著色窗口中看到。

封閉之後,線路

fly();
spider();
bird();
cat();

sing()函數將按照給定的順序調用這些函數。 然後,完成sing()函數的工作。


92
投票

dlaliberte第一點的示例:

閉包不僅在您返回內部函數時創建。 實際上,封閉函數根本不需要返回。 您可以改為將內部函數分配給外部作用域中的變量,或將其作為參數傳遞給可以立即使用的另一個函數。 因此,在調用封閉函數時,封閉函數的關閉可能已經存在,因為任何內部函數都可以在調用它後立即對其進行訪問。

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);

200
投票

您可以向5歲的孩子解釋關閉嗎?*

我仍然認為Google的解釋非常有效且簡潔:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

證明此示例即使內部函數未返回也創建了閉合

* AC#問題


103
投票

我不明白為什麼答案在這裡這麼複雜。

這是一個閉包:

var a = 42;

function b() { return a; }

是。 您可能一天使用多次。


沒有理由相信閉包是解決特定問題的複雜設計方法。 不, 從函數聲明的位置(不運行)的角度來看 ,閉包只是使用來自更高範圍的變量。

現在,它允許您執行的操作會更加壯觀,請參閱其他答案。


48
投票

也許除了六歲的孩子中最早熟的孩子之外,還有其他一些例子,但有一些例子使我對JavaScript的關閉概念感到滿意。

閉包是可以訪問另一個函數的作用域(其變量和函數)的函數。 創建閉包的最簡單方法是在函數中使用一個函數。 原因是在JavaScript中,函數始終可以訪問其包含函數的作用域。

 function outerFunction() { var outerVar = "monkey"; function innerFunction() { alert(outerVar); } innerFunction(); } outerFunction(); 

警報:猴子

在上面的示例中,調用了externalFunction,依次調用了innerFunction。 請注意,innerFunction如何使用outerVar,可以通過正確警告outerVar的值來證明。

現在考慮以下幾點:

 function outerFunction() { var outerVar = "monkey"; function innerFunction() { return outerVar; } return innerFunction; } var referenceToInnerFunction = outerFunction(); alert(referenceToInnerFunction()); 

警報:猴子

referenceToInnerFunction設置為outerFunction(),它僅返回對innerFunction的引用。 調用referenceToInnerFunction時,它將返回outerVar。 再次,如上所述,這證明了innerFunction可以訪問outsideVar(outerFunction的變量)。 此外,有趣的是,即使在externalFunction完成執行後,它仍保留該訪問權限。

在這裡,事情變得非常有趣。 如果要擺脫outerFunction,例如將其設置為null,您可能會認為referenceToInnerFunction將失去對outerVar值的訪問。 但這種情況並非如此。

 function outerFunction() { var outerVar = "monkey"; function innerFunction() { return outerVar; } return innerFunction; } var referenceToInnerFunction = outerFunction(); alert(referenceToInnerFunction()); outerFunction = null; alert(referenceToInnerFunction()); 

警報:猴子警報:猴子

但是怎麼回事? 既然將outerFunction設置為null,referenceToInnerFunction仍如何知道outerVar的值?

referenceToInnerFunction仍然可以訪問outerVar的值的原因是,當通過將innerFunction放置在outerFunction的內部來首次創建閉包時,innerFunction在其作用域鏈中添加了對outerFunction範圍(其變量和函數)的引用。 這意味著innerFunction具有指向所有outerFunction變量(包括outerVar)的指針或引用。 因此,即使outerFunction完成執行,或者即使將其刪除或設置為null,其作用域中的變量(例如outerVar)也會停留在內存中,因為在internalFunction部分上對它們的出色引用referenceToInnerFunction。 為了從內存中真正釋放outerVar和outerFunction的其餘變量,您必須擺脫對它們的傑出引用,例如通過將referenceToInnerFunction也設置為null。

//////////

關於閉包要注意的另外兩件事。 首先,閉包將始終可以訪問其包含函數的最後一個值。

 function outerFunction() { var outerVar = "monkey"; function innerFunction() { alert(outerVar); } outerVar = "gorilla"; innerFunction(); } outerFunction(); 

警報:大猩猩

其次,在創建閉包時,它會保留對其所有封閉函數的變量和函數的引用; 它沒有選擇。 但是,因此,應該謹慎使用閉包,或者至少要謹慎使用閉包,因為它們可能佔用大量內存; 在包含函數完成執行後很長一段時間內,許多變量可以保留在內存中。


127
投票

即使父母走了,孩子們也將永遠記住與父母分享的秘密。 這就是函數的閉包。

JavaScript函數的秘密是私有變量

var parent = function() {
 var name = "Mary"; // secret
}

每次調用它時,都會創建局部變量“ name”,並給定名稱“ Mary”。 並且每次函數退出時,變量都會丟失,並且名稱也將被忘記。

您可能會猜到,因為每次調用函數時都會重新創建變量,並且沒人會知道它們,所以必須在一個秘密的地方存儲它們。 可以將其稱為“密室”或“ 堆棧”或“ 本地範圍”,但這並不重要。 我們知道它們在那裡,藏在內存中。

但是,在JavaScript中,有一個非常特殊的東西,即在其他函數內部創建的函數也可以知道其父級的局部變量,並在它們存在之前一直保留它們。

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

因此,只要我們處於父函數中,它就可以創建一個或多個子函數,這些子函數確實共享秘密位置中的秘密變量。

但是,令人遺憾的是,如果子項也是其父函數的私有變量,則它也將在父項結束時死亡,而秘密也將隨之消失。

為了生存,孩子必須在為時已晚之前離開

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

現在,即使瑪麗“不再跑步”,她的記憶也不會丟失,她的孩子將永遠記住她的名字和他們在一起時分享的其他秘密。

因此,如果您稱孩子“愛麗絲”,她會回應

child("Alice") => "My name is Alice, child of Mary"

這就是全部。


76
投票

Closures的作者很好地解釋了閉包,解釋了我們為什麼需要它們的原因,還解釋了LexicalEnvironment,這對於理解閉包是必需的。
這是摘要:

如果訪問變量但不是局部變量怎麼辦? 像這兒:

在此處輸入圖片說明

在這種情況下,解釋器在外部LexicalEnvironment對像中找到變量。

該過程包括兩個步驟:

  1. 首先,創建函數f時,不會在空白處創建它。 當前有一個LexicalEnvironment對象。 在上述情況下,它是一個窗口(函數創建時未定義a)。

在此處輸入圖片說明

創建函數時,它會獲得一個名為[[Scope]]的隱藏屬性,該屬性引用當前的LexicalEnvironment。

在此處輸入圖片說明

如果讀取了變量,但在任何地方都找不到,則會生成錯誤。

嵌套函數

函數可以彼此嵌套,形成LexicalEnvironments鏈,也可以稱為作用域鏈。

在此處輸入圖片說明

因此,函數g可以訪問g,a和f。

關閉

外部函數完成後,嵌套函數可能會繼續存在:

在此處輸入圖片說明

標記詞法環境:

在此處輸入圖片說明

如我們所見, this.say是用戶對this.say中的一個屬性,因此它在用戶完成後繼續存在。

而且,如果您還記得,當創建this.say時,它(作為每個函數)將獲得內部this.say.[[Scope]]到當前LexicalEnvironment的內部引用。 因此,當前User執行的LexicalEnvironment保留在內存中。 User的所有變量也都是其屬性,因此也要小心保留它們,而不是像平常一樣。

關鍵是要確保內部函數將來要訪問外部變量,它能夠這樣做。

總結一下:

  1. 內部函數保留對外部LexicalEnvironment的引用。
  2. 即使外部函數完成,內部函數也可以隨時從中訪問變量。
  3. 瀏覽器將LexicalEnvironment及其所有屬性(變量)保留在內存中,直到有內部函數引用它為止。

這稱為關閉。


237
投票

不久前,我寫了一篇博客文章解釋了閉包。 這就是我說的關於閉包的原因因為您想要一個閉包。

閉包是一種讓函數擁有持久性私有變量的方法,也就是說,只有一個函數才知道的變量,它可以在其中跟踪以前運行時的信息。

從這種意義上講,它們使函數的行為有點像具有私有屬性的對象。

全文:

那麼這些閉包到底是什麼呢?


79
投票

JavaScript函數可以訪問它們:

  1. 爭論
  2. 局部變量(即它們的局部變量和局部函數)
  3. 環境,包括:
    • 全球,包括DOM
    • 外部功能中的任何東西

如果函數訪問其環境,則該函數為閉包。

注意,外部函數不是必需的,儘管它們確實提供了好處,我在這裡不討論。 通過訪問其環境中的數據,閉包可以使該數據保持活動狀態。 在外部/內部函數的子情況下,外部函數可以創建本地數據並最終退出,但是,如果任何內部函數在外部函數退出後仍然存在,則內部函數將保留外部函數的本地數據活。

使用全局環境的閉包示例:

想像一下,堆棧溢出投票和投票下降按鈕事件是作為閉包,votUp_click和voteDown_click實現的,它們可以訪問全局定義的外部變量isVotedUp和isVotedDown。 (為簡單起見,我指的是StackOverflow的“問題投票”按鈕,而不是“答案投票”按鈕的數組。)

當用戶單擊VoteUp按鈕時,voteUp_click函數將檢查isVotedDown == true,以確定是投票還是只取消反對票。 函數votUp_click是一個閉包,因為它正在訪問其環境。

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

所有這四個功能都是閉包,因為它們都訪問環境。


2388
投票

前言:當問題是:

就像老阿爾伯特(Albert)所說:“如果您不能向六歲的孩子解釋它,那麼您自己真的不理解。”好吧,我試圖向一位27歲的朋友解釋JS的關閉,但完全失敗了。

有人可以認為我6歲並對這個主題感到奇怪嗎?

我敢肯定,我是唯一嚐試從字面上回答最初問題的人之一。 從那時起,這個問題已經改變了幾次,所以我的答案現在似乎變得非常愚蠢和不合適。 希望這個故事的總體思路對某些人仍然很有趣。


在解釋困難的概念時,我非常喜歡類比和隱喻,所以讓我嘗試一個故事。

很久以前:

有一位公主

function princess() {

她生活在一個充滿冒險的奇妙世界中。 她遇到了白馬王子,騎著獨角獸環遊世界,與巨龍作戰,遇到了會說話的動物,以及許多其他奇幻的事物。

    var adventures = [];

    function princeCharming() { /* ... */ }

    var unicorn = { /* ... */ },
        dragons = [ /* ... */ ],
        squirrel = "Hello!";

    /* ... */

但是她總是必須回到繁瑣的瑣事和大人的世界。

    return {

而且她經常會告訴他們她作為公主的最新奇妙冒險。

        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}

但是他們只會看到一個小女孩...

var littleGirl = princess();

...講述魔術和幻想的故事

littleGirl.story();

即使大人們知道真正的公主,他們也永遠不會相信獨角獸或龍,因為他們永遠看不到它們。 大人們說,它們只存在於小女孩的想像力之內。

但是我們知道真實的事實; 那個里面有公主的小女孩...

...真的是一個里面有一個小女孩的公主。


83
投票

您正在睡覺,請Dan。 您告訴Dan帶一個XBox控制器。

丹邀請保羅。 丹請保羅帶一名管制員。 有多少控制者參加了聚會?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");

138
投票

我整理了一個交互式JavaScript教程,以解釋閉包是如何工作的。 什麼是封閉?

這是示例之一:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here

165
投票

關於關閉的維基百科

在計算機科學中,閉包是一個函數,以及對該函數的非本地名稱(自由變量)的引用環境。

從技術上講,在JavaScript中每個函數都是一個閉包 它始終可以訪問在周圍範圍內定義的變量。

由於JavaScript中的作用域定義構造是一個函數 ,而不是許多其他語言中的代碼塊,因此JavaScript中的閉包通常指的是一個函數,函數使用已執行的周圍函數中定義的非局部變量

閉包通常用於創建帶有一些隱藏的私有數據的函數(但並非總是如此)。

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

EMS

上面的示例使用一個匿名函數,該函數執行一次。 但這不是必須的。 可以命名它(例如mkdb )並在以後執行,每次調用它都會生成一個數據庫函數。 每個生成的函數都有其自己的隱藏數據庫對象。 閉包的另一個用法示例是當我們不返回一個函數,而是一個對象,該對象包含出於不同目的的多個函數,這些函數中的每個函數都可以訪問相同的數據。


735
投票

認真對待這個問題,我們應該找出一個典型的6歲孩子在認知上有什麼能力,儘管可以肯定的是,對JavaScript感興趣的人並不是那麼典型。

關於兒童發展:5至7歲,它說:

您的孩子將能夠遵循兩步指導。 例如,如果您對孩子說“去廚房給我一個垃圾袋”,他們將能夠記住該方向。

我們可以使用該示例來說明閉包,如下所示:

廚房是一個有局部變量的封閉trashBags ,稱為trashBags 廚房內部有一個名為getTrashBag的函數,該函數獲取一個垃圾袋並返回。

我們可以這樣在JavaScript中進行編碼:

 function makeKitchen() { var trashBags = ['A', 'B', 'C']; // only 3 at first return { getTrashBag: function() { return trashBags.pop(); } }; } var kitchen = makeKitchen(); console.log(kitchen.getTrashBag()); // returns trash bag C console.log(kitchen.getTrashBag()); // returns trash bag B console.log(kitchen.getTrashBag()); // returns trash bag A 

進一步說明了為什麼閉包有趣的原因:

  • 每次調用makeKitchen() ,都會使用其自己的單獨的trashBags創建一個新的閉包。
  • trashBags變量是每個廚房內部的局部變量,外部不能訪問,但是getTrashBag屬性的內部函數可以訪問該getTrashBag
  • 每個函數調用都會創建一個閉包,但是除非可以從閉包外部調用可以訪問閉包內部的內部函數,否則無需保持閉包。 在這裡使用getTrashBag函數返回對象。

©2020 sofbug.com - All rights reserved.