閉包解釋 JavaScript閉包如何工作?




閉包解釋 (24)

也許除了最早熟的六歲孩子之外,還有一些例子,但是有一些例子幫助我在JavaScript中實現了關閉概念。

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

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

outerFunction();

警告:猴子

在上面的例子中,調用了outerFunction,後者又調用了innerFunction。注意innerVar如何可用於innerFunction,通過正確警告outerVar的值來證明。

現在考慮以下內容:

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

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

警告:猴子

referenceToInnerFunction設置為outerFunction(),它只返回對innerFunction的引用。當調用referenceToInnerFunction時,它返回outerVar。同樣,如上所述,這表明innerFunction可以訪問outerVar的變量outerVar。此外,有趣的是,即使在outerFunction完成執行後,它仍保留此訪問權限。

在這裡,事情變得非常有趣。如果我們要刪除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)仍會留在內存中,因為在返回到的內部函數部分對它們的未完成引用referenceToInnerFunction。要從內存中真正釋放outerVar和其餘的outerFunction變量,你必須擺脫對它們的這種出色的引用,通過將referenceToInnerFunction設置為null也可以。

//////////

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

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

    innerFunction();
}

outerFunction();

警告:大猩猩

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

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

我已經看過維基百科上給出的Scheme示例 ,但遺憾的是它並沒有幫助。


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

// 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

閉包很難解釋,因為它們被用來使某些行為工作,每個人都直觀地期望工作。 我發現解釋它們的最佳方式(以及學習它們的方式)是想像沒有它們的情況:

    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。


閉包很簡單:

以下簡單示例涵蓋了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都會創建一個新的局部變量n,該變量在返回後很長時間內仍然可以被該計算器addmultiply函數使用make_calculator

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

像內部函數addmultiply,其訪問外部函數聲明的變量**,被稱為閉包

這幾乎就是閉包。


*例如,它涵蓋了另一個答案中給出的“閉包傻瓜”一文中的所有要點,除了示例6,它簡單地表明變量可以在聲明之前使用,這是一個很好的事實,但是與閉包完全無關。它還涵蓋了接受答案中的所有要點,除了函數將其參數複製到局部變量(命名函數參數)的點(1),以及(2)複製數字創建新數字,但複製對象引用為您提供對同一對象的另一個引用。這些也很好知道但又與封閉完全無關。它也非常類似於這個答案中的例子,但是更短,更抽象。它沒有涉及到這一點這個答案或者這個評論,就是說JavaScript很難堵塞當前的內容循環變量的值到內部函數中:“插入”步驟只能通過包含內部函數的輔助函數來完成,並在每次循環迭代時調用。 (嚴格地說,內部函數訪問輔助函數的變量副本,而不是插入任何東西。)同樣,在創建閉包時非常有用,但不是閉包的一部分或它是如何工作的。由於閉包在ML等函數式語言中的工作方式不同,還存在額外的混淆,其中變量綁定到值而不是存儲空間,從而提供了一種持續理解閉包的人(即“插入”方式),即只是對JavaScript不正確,其中變量總是綁定到存儲空間,而永遠不會綁定到值。

**任何外部函數,如果有幾個嵌套,或甚至在全局上下文中,正如這個答案清楚地指出的那樣。


稻草人

我需要知道點擊一個按鈕多少次,每三次點擊就做一些事......

相當明顯的解決方案

// 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的閉包行為。 此行為允許任何函數無限期地訪問創建它的作用域。 為了實際應用這個,我立即調用一個返回另一個函數的函數,因為我正在返回的函數可以訪問內部計數變量(由於上面解釋的閉包行為),這導致了一個私有範圍供結果使用功能...不是那麼簡單? 讓我們淡化它......

一個簡單的單線封閉

//          _______________________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注意事項)


這是為了澄清關於某些其他答案中出現的閉包的幾個(可能的)誤解。

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

Closures的作者很好地解釋了閉包,解釋了為什麼我們需要它們,並解釋了理解閉包所必需的LexicalEnvironment。
以下是摘要:

如果訪問變量,但它不是本地的,該怎麼辦?像這兒:

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

該過程包括兩個步驟:

  1. 首先,當創建函數f時,不會在空白空間中創建它。有一個當前的LexicalEnvironment對象。在上面的例子中,它是窗口(在創建函數時a未定義)。

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

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

嵌套函數

函數可以嵌套在另一個函數中,形成一個LexicalEnvironments鏈,也可以稱為範圍鏈。

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

關閉

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

標記LexicalEnvironments:

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

如果你記得,什麼時候this.say創建,它(作為每個函數)獲得this.say.[[Scope]]當前LexicalEnvironment 的內部引用。因此,當前用戶執行的LexicalEnvironment保留在內存中。User的所有變量也都是它的屬性,因此它們也被仔細保存,而不是通常的垃圾。

重點是確保如果內部函數將來想要訪問外部變量,它就能夠這樣做。

總結一下:

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

這稱為閉包。


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

你知道成年人如何擁有房子,他們稱之為家?當一個媽媽有一個孩子,孩子真的沒有任何東西,對吧?但是它的父母擁有一所房子,所以每當有人問孩子“你的家在哪裡?”時,他/她就可以回答“那所房子!”,並指向其父母的房子。一個“關閉”是孩子總是(即使在國外)能夠說它有一個家的能力,即使它真的是父母擁有房子。


我在一段時間後寫了一篇博文,解釋了閉包。這就是我所說的關於你想要一個閉包的原因

閉包是一種讓函數具有持久的私有變量的方法 - 也就是說,只有一個函數知道的變量,它可以跟踪之前運行的信息。

從這個意義上講,它們讓函數有點像具有私有屬性的對象。

全文:

那麼封閉東西是什麼?


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

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

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

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

無論何時在較大範圍內創建較小的範圍,較小的範圍始終都能夠查看較大範圍內的範圍。


你能解釋一下5歲的閉嘴嗎?*

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

/*
*    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#問題


閉合很像一個對象。無論何時調用函數,它都會被實例化。

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

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

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

這將始終記錄16,因為bar可以訪問被定義為foo參數的x ,並且它還可以從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複製foo作為其參數x

另一方面,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對象的內存洩漏的基礎。


好的,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");


認真對待這個問題,我們應該找出一個典型的6歲孩子的認知能力,儘管如此,對JavaScript感興趣的人並不那麼典型。

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

您的孩子將能夠按照兩步指示。 例如,如果您對您的孩子說:“去廚房給我一個垃圾袋”,他們就能記住這個方向。

我們可以使用這個例子來解釋閉包,如下所示:

廚房是一個閉包,有一個局部變量,稱為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函數返回對象就可以了。

dlaliberte的第一點示例:

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

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

作為一個6歲的孩子的父親,目前正在教育幼兒(以及沒有接受過正規教育的相對新手,因此需要更正),我認為通過實際操作可以最好地吸取教訓。如果這個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  */  }

請注意,/*...*///停止瀏覽器讀取的代碼。

姓名:您可以根據自己想要的任何單詞調用函數。示例“cookMeal”通常是將兩個單詞連接在一起並在第二個單詞的開頭給出大寫字母 - 但這不是必需的。它不能有空間,它本身不能是一個數字。

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

支架:看起來像這樣的“支架” {}是我們工廠的有色窗戶。從工廠內部你可以看到,但從外面你看不到。

上面的長代碼示例

我們的代碼以函數一詞開頭,所以我們知道它是一個!然後函數的名稱唱歌 - 這是我自己對函數的描述。然後括號()。括號總是在那裡用於函數。有時他們是空的,有時候他們有東西。這個有一個字:(person)。在這之後有一個像這樣的支架{。這標誌著函數sing()的開始。它有一個夥伴,標誌著像這樣的唱歌結束}

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

所以這個功能可能與唱歌有關,可能需要一些人的數據。它內部有指令來處理該數據。

現在,在函數sing()之後,接近代碼的末尾就是行

var person="an old lady";

VARIABLE:字母var代表“變量”。變量就像一個信封。在外面,這個信封被標記為“人”。在內部它包含一張紙,上面有我們的功能需要的信息,一些字母和空格連在一起就像一根字符串(它被稱為字符串),使一個短語讀作“一位老太太”。我們的信封可能包含其他類型的東西,如數字(稱為整數),指令(稱為函數),列表(稱為數組)。因為這個變量是在所有大括號之外寫的{},並且因為當你在大括號內時你可以通過有色窗口看到,所以可以從代碼中的任何地方看到這個變量。我們稱之為“全局變量”。

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

在此之後我們有了這條線

sing(person);

這一行正在調用該函數,就好像它正在調用一隻狗一樣

“來,來吧,讓的人!”

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

功能定義動作 - 主要功能是唱歌。它包含一個名為firstPart的變量,它適用於歌曲中關於適用於歌曲每個節目的人的歌唱:“有”+人+“吞下”。如果您在控制台中鍵入firstPart,則無法獲得答案,因為該變量已鎖定在函數中 - 瀏覽器無法在大括號的有色窗口內看到。

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

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

關閉之後就行了

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

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


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


我整理了一個交互式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

適用於初學者的JavaScript閉包

莫里斯於星期二提交,2006-02-21 10:19。 社區編輯以來。

閉包不是魔術

這個頁面解釋了閉包,以便程序員可以理解它們 - 使用有效的JavaScript代碼。 它不適合大師或功能程序員。

一旦核心概念被弄清楚,關閉並不難理解。 但是,通過閱讀任何理論或學術導向的解釋,他們無法理解!

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

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

兩個簡短的摘要

  • 當函數(foo)聲明其他函數(bar和baz)時,在函數退出時不會銷毀在foo中創建的局部變量族。 這些變量只會變得對外界不可見。 因此,Foo可以狡猾地返回函數bar和baz,並且他們可以繼續通過這個封閉的變量系列(“封閉”)繼續讀取,寫入和通信,其他任何人都無法干預,甚至沒有人打電話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() ,則會創建一個新的閉包(stack-frame!)。 舊的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"

Tricky:還要注意, say變量也在閉包內,並且可以被任何其他可能在sayAlice()聲明的函數訪問,或者可以在inside函數內遞歸訪問。

例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的局部變量只有一個閉包( resultiitem )。 當在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

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

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是一個閉包,它將無法工作(當然,你甚至不會想到在運行時進行源代碼字符串替換,但是...)。
  • 可以在函數&mdash中的函數聲明中獲取函數聲明,並且可以在多個級別獲得閉包。
  • 我認為通常閉包是函數和捕獲的變量的術語。 請注意,我在本文中沒有使用該定義!
  • 我懷疑JavaScript中的閉包與函數式語言中的閉包有所不同。

鏈接

謝謝

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

莫里斯的原帖可以在互聯網檔案中找到。


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

閉包是具有狀態的功能。它有點類似於“this”,意思是“this”也為函數提供狀態但函數和“this”是單獨的對象(“this”只是一個奇特的參數,並且是將它永久綁定到a的唯一方法函數是創建一個閉包)。雖然“this”和函數總是分開存在,但函數不能與其閉包分離,並且語言無法訪問捕獲的變量。

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

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

一個例子:

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();

好的,和一個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來玩它。


你正在睡覺,你邀請丹。你告訴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.");

前言:這個答案是在問題是:

就像老阿爾伯特所說的那樣:“如果你不能解釋它給一個六歲的孩子,你自己真的不明白。”我試著向一位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();

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

但我們知道真相; 裡面有公主的小女孩......

......真是個公主,裡面有個小女孩。





closures