JavaScript 闭包如何工作?

您将如何向了解其闭包概念(例如函数,变量等)的人解释 JavaScript 闭包,但却不了解闭包本身?

我已经在 Wikipedia 上看到了 Scheme 示例 ,但是不幸的是它没有帮助。

答案

面向初学者的 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); } 另一个函数内部声明,在此示例中为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); // attention here: new closure assigned to a new variable!
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 存档中找到。

每当您在另一个函数中看到 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 对象内存泄漏的基础。

前言:当问题是:

就像老阿尔伯特(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();

即使大人们知道真正的公主,他们也永远不会相信独角兽或龙,因为他们永远看不到它们。大人们说,它们只存在于小女孩的想像力之内。

但是我们知道真实的事实;那个里面有公主的小女孩...

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

认真对待这个问题,我们应该找出一个典型的 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函数返回对象。

稻草人

我需要知道一个按钮被点击了多少次,并且每三次单击都会执行某项操作...

相当明显的解决方案

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

闭包很难解释,因为闭包用于使某些行为正常工作,每个人都希望它们能正常工作。我发现解释它们的最佳方法(以及了解它们的方法)是想象没有它们的情况:

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 中闭包的范围是词法的,这意味着闭包所属函数中包含的所有内容都可以访问其中的任何变量。

如果您在闭包中包含一个变量,

  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

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

这是为了消除对其他一些答案中出现的闭包的几种(可能的)误解。

  • 闭包不仅在您返回内部函数时创建。实际上,封闭函数根本不需要返回即可创建封闭函数。您可以改为将内部函数分配给外部作用域中的变量,或将其作为参数传递给另一个函数,在该函数中可以立即或在以后的任何时间调用它。因此,封闭函数的关闭很可能在调用封闭函数后立即创建因为只要在调用封闭函数之前或之后,任何内部函数都可以访问该封闭。
  • 闭包在其范围内未引用变量的旧值的副本。变量本身是闭包的一部分,因此访问这些变量之一时看到的值是访问它时的最新值。这就是为什么在循环内部创建内部函数会很棘手的原因,因为每个函数都可以访问相同的外部变量,而不是在创建或调用函数时获取变量的副本。
  • 闭包中的 “变量” 包括在函数内声明的任何命名函数。它们还包括函数的参数。闭包还可以访问其包含的闭包的变量,直到全局范围为止。
  • 闭包使用内存,但是它们不会导致内存泄漏,因为 JavaScript 本身会清理自己的未引用的循环结构。当 Internet Explorer 无法断开引用闭包的 DOM 属性值的连接时,会创建涉及闭包的 Internet Explorer 内存泄漏,从而维护对可能的圆形结构的引用。

不久前,我写了一篇博客文章解释了闭包。这就是我说的关于闭包的原因因为您想要一个闭包。

闭包是一种使函数具有永久性私有变量的方法,也就是说,只有一个函数才知道的变量,它可以在其中跟踪以前运行时的信息。

从这种意义上讲,它们使函数的行为有点像具有私有属性的对象。

全文:

那么这些闭包到底是什么呢?