JavaScript闭包如何工作?


7636

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

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


391
我对这些问题和许多答案的问题是,它们从抽象的理论角度进行处理,而不是从简单地解释为什么在Javascript中需要闭包以及使用它们的实际情况开始。最后,您必须撰写一篇tl; dr文章,一直在思考,“但是,为什么?”。我将从以下内容开始:闭包是处理JavaScript的以下两个现实的巧妙方法:范围是在功能级别上,而不是在块级别上; b。您实际上在JavaScript中所做的大部分工作都是异步/事件驱动的。
杰里米·伯顿

53
@Redsandro例如,它使事件驱动的代码易于编写。页面加载时,我可能会触发一个函数来确定有关HTML或可用功能的详细信息。我可以在该函数中定义和设置处理程序,并且每次调用该处理程序时都可以使用所有上下文信息,而不必重新查询它。解决问题一次,在需要处理程序的每个页面上重复使用,以减少处理程序重新调用的开销。您曾经看到相同的数据用没有它们的语言重新映射过两次吗?封闭使得避免这种事情变得容易得多。
Erik Reppen 2013年

1
@Erik Reppen谢谢您的回答。实际上,我对这种难以阅读的closure代码的好处感到好奇Object Literal,与之相反,它可以重用自身并减少开销,但包装代码却减少了100%。
Redsandro 2013年

6
对于Java程序员来说,简短的答案是它等同于内部类的功能。内部类还持有一个指向外部类实例的隐式指针,并用于几乎相同的目的(即创建事件处理程序)。
Boris van Schooten 2014年

8
我发现这个实际示例非常有用:youtube.com/watch?
v

Answers:


7357

闭包是以下内容的配对:

  1. 一个功能,以及
  2. 对函数外部范围的引用(词法环境)

词法环境是每个执行上下文(堆栈框架)的一部分,并且是标识符(即局部变量名称)和值之间的映射。

JavaScript中的每个函数都对其外部词汇环境保持引用。此引用用于配置调用函数时创建的执行上下文。该引用使函数内部的代码可以“查看”在函数外部声明的变量,而不管调用函数的时间和位置。

如果一个函数被一个函数调用,而另一个函数又调用了另一个函数,则将创建对外部词法环境的引用链。该链称为作用域链。

在下面的代码中,innerfoo调用时创建的执行上下文的词法环境形成一个闭包,并覆盖变量secret

function foo() {
  const secret = Math.trunc(Math.random()*100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`

换句话说:在JavaScript中,函数带有对私有“状态框”的引用,只有它们(以及在相同词法环境中声明的任何其他函数)才能对其进行访问。状态框对于函数的调用者是不可见的,从而为数据隐藏和封装提供了一种出色的机制。

请记住:JavaScript中的函数可以像变量一样传递(一流的函数),这意味着功能和状态对可以在程序中传递:类似于您在C ++中传递类的实例的方式。

如果JavaScript没有闭包,则必须在函数之间显式传递更多状态,从而使参数列表更长,代码更嘈杂。

因此,如果您希望函数始终有权访问私有状态,则可以使用闭包。

......频频我们希望与功能关联状态。例如,在Java或C ++中,当您将私有实例变量和方法添加到类时,您会将状态与功能相关联。

在C语言和大多数其他常见语言中,函数返回后,所有局部变量将不再可访问,因为堆栈框架被破坏了。在JavaScript中,如果在另一个函数中声明一个函数,则外部函数从其返回后仍可访问。这样一来,在上面的代码,secret仍然可用的函数对象inner之后它已经从返回foo

闭包的使用

每当您需要与函数关联的私有状态时,闭包都是有用的。这是一个非常常见的情况-请记住:JavaScript直到2015年才使用类语法,并且仍然没有私有字段语法。封闭件可满足此需求。

私有实例变量

在以下代码中,函数toString关闭了汽车的详细信息。

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}
const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver')
console.log(car.toString())

功能编程

在以下代码中,函数同时inner关闭fnargs

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

面向事件的编程

在下面的代码中,函数onClick在variable之上关闭BACKGROUND_COLOR

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200,200,242,1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)
<button>Set background color</button>

模块化

在下面的示例中,所有实现细节都隐藏在立即执行的函数表达式中。这些功能tick以及toString它们完成工作所需的私有状态和功能关闭。封闭使我们能够模块化和封装我们的代码。

let namespace = {};

(function foo(n) {
  let numbers = []
  function format(n) {
    return Math.trunc(n)
  }
  function tick() {
    numbers.push(Math.random() * 100)
  }
  function toString() {
    return numbers.map(format)
  }
  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())

例子

例子1

此示例显示局部变量未在闭包中复制:闭包维护对原始变量本身的引用。似乎即使外部函数退出后,堆栈框架仍在内存中保留。

function foo() {
  let x = 42
  let inner  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

例子2

在下面的代码,三种方法logincrementupdate所有密切在同一词法环境。

每次createObject调用时,都会创建一个新的执行上下文(堆栈框架),并创建一个全新的变量x,并创建一组新的函数(log等),这些新函数将覆盖该新变量。

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

例子3

如果您使用的是使用声明的变量var,请务必了解要关闭的变量。使用声明的变量var被提升。由于引入了let和,这在现代JavaScript中几乎没有问题。const

在以下代码中,每次循环时,inner都会创建一个新函数,该函数关闭i。但是由于var i悬挂在循环外部,所有这些内部函数都在同一个变量上闭合,这意味着将i(3)的最终值打印了三遍。

function foo() {
  var result = []
  for (var i = 0; i < 3; i++) {
    result.push(function inner() { console.log(i) } )
  }
  return result
}

const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
  result[i]() 
}

最后一点:

  • 每当在JavaScript中声明函数时,都会创建一个闭包。
  • 返回一个 function从另一个函数内部是闭包的经典示例,因为外部函数内部的状态对于返回的内部函数隐式可用,即使在外部函数完成执行之后也是如此。
  • 每当eval()在函数内部使用时,都会使用闭包。您eval可以引用该函数的局部变量的文本,在非严格模式下,您甚至可以使用以下命令创建新的局部变量eval('var foo = …')
  • new Function(…)函数内部使用(函数构造函数)时,它不会关闭其词法环境:而是关闭全局上下文。新函数不能引用外部函数的局部变量。
  • JavaScript中的闭包就像保留引用(NOT在函数声明时保留对作用域副本),这继而保留对外部作用域的引用,依此类推,一直指向全局对象的顶部。范围链。
  • 声明函数时创建一个闭包。当调用函数时,此闭包用于配置执行上下文。
  • 每次调用函数时都会创建一组新的局部变量。

链接


74
这听起来不错:“ JavaScript的关闭就像保留所有局部变量的副本一样,就像它们退出函数时一样。” 但这是有误导性的,原因有两个。(1)函数调用不必退出即可创建闭包。(2)不是局部变量的副本,而是变量本身。(3)没有说谁有权访问这些变量。
dlaliberte

27
示例5显示了一个“陷阱”,其中的代码无法按预期工作。但是它没有显示如何修复它。 这另一个答案显示了一种解决方法。
马特

190
我喜欢这篇文章如何以大胆的大写字母“ Closures Are Not Magic”开头,并以“魔术是JavaScript中的函数引用也对创建它的闭包进行秘密引用”结束了第一个示例。
Andrew Macheret 2014年

6
Example#3是将闭包与javascripts提升混合在一起。现在,我认为仅解释闭合件就足够困难而又不引起起重行为。这对我最大的帮助:Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.来自developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
caramba

3
ECMAScript 6可能会在这篇有关闭包的出色文章中有所改变。例如,如果您使用let i = 0而不是var i = 0示例5,则testList()将会打印您最初想要的内容。
尼尔(Nier)

3988

JavaScript中的每个函数都维护与其外部词法环境的链接。词汇环境是作用域内所有名称(例如变量,参数)及其值的映射。

因此,每当您看到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和变量tmp,两者都存在于外部函数的词法环境中foo

Function bar及其与函数的词法环境的链接foo是一个闭包。

函数不必返回即可创建闭包。仅仅依靠其声明,每个函数都会在其封闭的词法环境中关闭,从而形成一个闭合。

function foo(x) {
  var tmp = 3;

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

var bar = foo(2);
bar(10); // 16
bar(10); // 17

上面的函数也会记录16,因为里面的代码bar仍然可以引用参数x和变量tmp,即使它们不再直接在范围内。

但是,由于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函数时,将ec创建一个新的执行上下文。与函数参数和目标对象一起,此执行上下文还接收到调用执行上下文的词法环境的链接,这意味着可以从中获得在外部词法环境中声明的变量(在上面的示例中,abec

每个函数都会创建一个闭包,因为每个函数都具有与其外部词法环境的链接。

请注意,变量本身在闭包内部可见,而不是副本。


24
@feeela:是的,每个JS函数都会创建一个闭包。在现代JS引擎中,未引用的变量可能有资格进行垃圾回收,但是这并没有改变以下事实:创建执行上下文时,该上下文引用了封闭的执行上下文及其变量,并且该函数是一个对象,有可能被重新定位到其他变量范围,同时保留该原始引用。那就是关闭。

@Ali我刚刚发现我提供的jsFiddle实际上并不能证明任何事情,因为delete失败了。但是,执行定义该函数的语句时,将确定该函数将作为[[Scope]]携带的词法环境(并最终在被调用时用作其自身词法环境的基础)。这意味着该函数关闭正在执行的作用域的整个内容,而不管其实际引用的是哪个值以及是否对作用域进行转义。请看看部分13.2和10 的规格
阿萨德Saeeduddin

8
在尝试解释原始类型和引用之前,这是一个很好的答案。它完全是错误的,并讨论了要复制的文字,这实际上与任何内容都没有关系。
Ry-

12
闭包是JavaScript对基于类,面向对象的编程的回答。JS不是基于类的,因此人们不得不寻找另一种方式来实现某些无法实现的事情。
巴特洛梅耶扎莱夫斯基

2
这应该是公认的答案。魔术永远不会在内部功能中发生。将外部函数分配给变量时会发生这种情况。这为内部函数创建了一个新的执行上下文,因此可以累积“私有变量”。当然,由于分配给外部函数的变量可以保持上下文。第一个答案只是使整个事情变得更加复杂,而没有解释那里真正发生的事情。
Albert Gao

2442

前言:当问题是:

就像老阿尔伯特所说:“如果您不能向六岁的孩子解释它,那么您自己真的不理解。”好吧,我试图向一个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();

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

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

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


340
我真的很喜欢这个解释。对于那些阅读它却不遵循它的人来说,这是类似的:princess()函数是一个包含私有数据的复杂范围。在该功能之外,无法查看或访问私有数据。公主在她的想象力中保留了独角兽,巨龙,冒险等(私人数据),大人们自己看不见它们。但是,公主的想象力被捕获在该story()函数的闭包中,这是该littleGirl实例暴露给魔术世界的唯一界面。
Patrick M

所以这里story是闭包,但是如果代码var story = function() {}; return story;当时littleGirl是闭包。至少那是MDN使用带有闭包的“私有”方法给我的印象:“这三个公共函数是共享同一环境的闭包”。
icc97 '16

16
@ icc97是的,story是一个闭包,引用的范围内提供的环境princessprincess也是另一个隐含的闭包,即princesslittleGirl将共享对parentslittleGirl存在和princess被定义的环境/范围内存在的数组的任何引用。
Jacob Swartwood '16

6
@BenjaminKrupp我添加了一个明确的代码注释,以显示/暗示主体中的操作princess比编写的要多。不幸的是,这个故事现在在这个线程上有点不合适。最初的问题是要“向5岁的孩子解释JavaScript的关闭”;我的回应是唯一尝试这样做的回应。我毫不怀疑它会惨遭失败,但是至少这种回应也许有机会抓住5岁儿童的利益。
雅各布·斯沃特伍德

11
实际上,对我而言,这是完全合理的。我必须承认,最终通过使用公主和冒险故事来理解JS闭包,这让我感到有点奇怪。
结晶

753

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

6
实际上,令人困惑的是,makeKitchen函数调用是实际的关闭,而不是它返回的厨房对象。
dlaliberte

6
在遍历其他人的过程中,我找到了这个答案,这是解释什么是闭包以及为什么闭包的最简单方法。
Chetabahana

3
菜单和开胃菜太多,肉和土豆不足。您可以只用一个简短的句子来改善该答案,例如:“闭包是函数的密封上下文,因为缺少类提供的任何作用域机制。”
Staplerfahrer

584

稻草人

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

相当明显的解决方案

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


11
我不同意您对闭包的定义。没有理由必须是自调用的。说必须“退回”也有点简单(且不准确)(对此问题的最佳答案的评论中对此进行了大量讨论)
James Montagne

40
@James即使您不同意,他的榜样(以及整个帖子)也是我见过的最好的榜样之一。虽然这个问题对我来说不是老问题,但可以解决,但完全值得+1。
e-satis

84
“我需要知道一个按钮被单击了多少次,并且每单击三次就执行一次操作……”这引起了我的注意。一个用例和解决方案显示了闭包如何不是一件神秘的事情,并且我们很多人都在写它们,但并不完全知道其正式名称。
克里斯22年

不错的示例,因为它表明第二个示例中的“计数”保留了“计数”的值,并且每次单击“元素”时都不会重置为0。非常翔实!
亚当

+1为关闭行为。我们可以将闭包行为限制为javascript中的函数,还是可以将此概念也应用于该语言的其他结构?
Dziamid

492

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

    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。


11
正是这样的示例使许多人误以为是返回函数中使用的,而不是可变变量本身。如果将其更改为“ return x + = y”,或者将其更改为另一个函数“ x * = y”,则很明显,没有任何内容被复制。对于习惯于堆叠框架的人们,可以想象使用堆框架,而堆框架在函数返回后可以继续存在。
马特

14
@马特我不同意。一个示例不应详尽记录所有属性。它旨在简化并说明概念的显着特征。OP要求进行简单的解释(“针对六岁儿童”)。接受公认的答案:它完全无法提供简洁的解释,恰恰是因为它试图穷举。(我确实同意您的观点,即绑定是引用而不是值的绑定,这是JavaScript的一个重要属性,但是再次,成功的解释是将其减少到最低限度。)
Konrad Rudolph 2013年

@KonradRudolph我喜欢您的示例的样式和简洁。我只是建议稍微对其进行更改,以使最后一部分“唯一的解决方案是...”成为现实。实际上,实际上,目前存在针对您的方案的另一种更简单的解决方案,该解决方案对应于javascript延续,并且确实对应于关于什么是延续的常见误解。因此,当前形式的示例很危险。这与详尽列出属性无关,而与理解返回函数中的x到底有关,这毕竟是要点。
马特2013年

@Matt Hmm,我不确定我是否完全理解您,但是我开始发现您可能有一个正确的观点。由于评论太短,您能否在要点/糕点或聊天室中解释您的意思?谢谢。
Konrad Rudolph 2013年

2
@KonradRudolph我想我不清楚x + = y的目的。目的只是为了表明重复调用返回的函数将继续使用相同的变量 x(与创建函数时人们想象的“插入” 相同的相反)。就像小提琴中的前两个警报一样。附加函数x * = y的目的是表明多个返回的函数都共享相同的x。
马特

376

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


26
发挥出色并回答原始海报。我认为这是最好的答案。我将以类似的方式使用行李:假设您去奶奶家,然后将Nintendo DS手提箱和游戏卡一起装在手提箱中,然后将手提箱包装在背包中,然后将游戏卡放在背包的口袋中,然后,您将整个东西放在一个大手提箱里,手提箱里放着更多的游戏卡。到奶奶家时,只要所有外部箱子都打开,您就可以在DS上玩任何游戏。或类似的东西。
slartibartfast 2013年

376

TLDR

闭包是函数与其外部词汇(即,编写的)环境之间的链接,这样,无论何时或从何时,从函数内部都可以看到在该环境中定义的标识符(变量,参数,函数声明等)。调用函数的位置。

细节

在ECMAScript规范的术语中,可以说闭包是通过[[Environment]]引用每个函数对象来实现的,该对象指向在其中定义函数的词法环境

通过内部[[Call]]方法调用功能[[Environment]]时,功能对象上的引用将复制到新创建的执行上下文环境记录外部环境引用(堆栈框架)。

在以下示例中,函数f关闭了全局执行上下文的词法环境:

function f() {}

在以下示例中,函数h关闭了函数的词法环境g,而后者又关闭了全局执行上下文的词法环境。

function g() {
    function h() {}
}

如果内部函数由外部函数返回,则外部词法环境将在外部函数返回后仍然存在。这是因为如果最终调用内部函数,则外部词汇环境需要可用。

在以下示例中,函数j关闭了函数的词法环境i,这意味着在函数完成执行很长时间之后,x就可以从函数内部看到变量:ji

function i() {
    var x = 'mochacchino'
    return function j() {
        console.log('Printing the value of x, from within function j: ', x)
    }
} 

const k = i()
setTimeout(k, 500) // invoke k (which is j) after 500ms

在闭包中,外部词法环境中的变量本身可用,而不是副本。

function l() {
  var y = 'vanilla';

  return {
    setY: function(value) {
      y = value;
    },
    logY: function(value) {
      console.log('The value of y is: ', y);
    }
  }
}

const o = l()
o.logY() // The value of y is: vanilla
o.setY('chocolate')
o.logY() // The value of y is: chocolate

通过外部环境引用在执行上下文之间链接的词汇环境链形成一个 范围链,并定义了从任何给定函数可见的标识符。

请注意,为了提高清晰度和准确性,此答案已与原始答案大为不同。


56
哇,从来不知道您可以console.log像这样使用字符串替换。如果任何人有兴趣也有多种:developer.mozilla.org/en-US/docs/DOM/...
闪光

7
函数参数列表中的变量也是闭包的一部分(例如,不仅限于var)。
Thomas Eding

闭包听起来更像是对象和类等。不确定为什么很多人不比较这两者-对我们新手来说更容易学习!
almaruf

365

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

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

15
詹姆斯,我说过,闭包是在调用封闭函数时“大概”创建的,因为有可能实现可以将闭包的创建推迟到某个时候才决定绝对需要闭包。如果在封闭函数中未定义内部函数,则不需要封闭。因此,也许可以等到创建第一个内部函数之后,再从封闭函数的调用上下文中创建封闭体。
dlaliberte 2012年

9
@ Beetroot-Beetroot假设我们有一个内部函数,该内部函数在外部函数返回之前被传递给另一个函数,并假定我们还从外部函数返回了相同的内部函数。在两种情况下,它都是相同的函数,但是您要说的是,在外部函数返回之前,内部函数已“绑定”到调用堆栈,而在返回之后,内部函数突然绑定到了闭包。在两种情况下,它的行为都相同。语义是相同的,所以您不只是在谈论实现细节吗?
dlaliberte 2012年

7
@ Beetroot-Beetroot,感谢您的反馈,我很高兴您能想到。我仍然看不到外部函数的活动上下文和该上下文在函数返回时变为闭合时(如果我理解您的定义)之间的语义差异。内部函数无关紧要。垃圾回收无关紧要,因为内部函数会以任何一种方式维护对上下文/闭包的引用,而外部函数的调用者只是将其引用删除到调用上下文。但这使人们感到困惑,将其称为调用上下文可能更好。
dlaliberte 2012年

9
该文章很难阅读,但我认为它实际上支持了我所说的内容。它说:“闭包是通过返回功能对象或通过将对此类功能对象的引用直接分配给例如全局变量来形成的。” 我并不是说GC无关紧要。相反,由于存在GC,并且由于内部函数已附加到外部函数的调用上下文(或如文章所述的[[scope]]),因此外部函数调用是否返回,因为与内部函数的绑定无关紧要功能是重要的。
dlaliberte 2012年

3
好答案!您应该添加的一件事是,所有函数都覆盖了定义它们的执行范围的全部内容。它们是引用父作用域中的某些变量还是不引用这些变量都没有关系:对父作用域的词法环境的引用无条件存储为[[Scope]]。这可以从ECMA规范中有关函数创建的部分中看到。
Asad Saeeduddin

236

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

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

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

全文:

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


那么,使用此示例可以强调关闭的主要好处吗?说我有一个函数emailError(sendToAddress,errorString)然后我可以说devError = emailError("devinrhode2@googmail.com", errorString)然后拥有自己的共享emailError函数的自定义版本?
Devin G Rhode

这种解释以及与(closure whats)链接相关的完美示例是理解闭包的最佳方法,应该放在最上面!
HopeKing

215

闭包很简单:

以下简单示例涵盖了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

如果您熟悉堆栈框架,这些计算器似乎很奇怪:退货n后如何继续访问它们make_calculator?答案是想象JavaScript不使用“堆栈框架”,而是使用“堆框架”,该堆可以在使它们返回的函数调用之后持续存在。

内部功能如 addmultiply这样的称为闭包(closures),它们访问在外部函数**中声明的变量。

这几乎是闭包的全部内容。



*例如,它涵盖了“封闭式假人”一文中的所有要点另一个答案中,例6除外,该示例仅表明变量可以在声明之前使用,这是一个很好的事实,但与闭合完全无关。它还涵盖了接受的答案所有要点,除了以下几点:(1)函数将其参数复制到局部变量(命名的函数参数),以及(2)复制数字创建一个新数字,但复制对象引用给您另一个对同一对象的引用。这些也是很好知道的,但又与闭包完全无关。它也非常类似于此答案,的示例但更简短,更抽象。它不涵盖重点这个答案当前这个评论,这是因为JavaScript使其难以插入将一个循环变量的值添加到内部函数中:“插入”步骤只能使用一个辅助函数来完成,该函数将您的内部函数封装起来,并在每次循环迭代时调用。(严格来说,内部函数访问变量的帮助器函数的副本,而不是插入任何内容。)再次,在创建闭包时非常有用,但不是闭包的一部分或工作方式的一部分。由于闭包在ML之类的功能语言中的工作方式不同,还存在其他混乱,在这种语言中,变量绑定到值而不是存储空间,从而提供了源源不断的了解闭包的人(即“插入”方式),即对于JavaScript而言,这是完全不正确的,因为JavaScript总是将变量绑定到存储空间,而不是绑定到值。

**任何外部函数(如果有多个函数嵌套的话),甚至在全局上下文中,正如该答案清楚指出的那样。


如果您调用以下内容,将会发生什么:second_calculator = first_calculator(); 而不是second_calculator = make_calculator(); ?应该一样吧?
罗南·费斯汀格

4
@Ronen:由于first_calculator是对象(不是函数),因此不应在中使用括号second_calculator = first_calculator;,因为它是赋值,而不是函数调用。为了回答您的问题,将只有一个调用make_calculator,因此只会生成一个计算器,并且变量first_calculator和second_calculator都引用同一个计算器,因此答案将分别为3、403、4433、44330。
马特

204

我如何向六岁的孩子解释:

您知道大人如何拥有房屋,他们称之为房屋吗?当妈妈有孩子时,孩子实际上并不拥有任何东西,对吗?但是它的父母拥有一所房子,因此只要有人问孩子“你的房子在哪里?”,他/她就可以回答“那所房子!”,并指向其父母的房子。“关闭”是孩子始终(即使在国外)也能够说自己拥有房屋的能力,即使这实际上是父母拥有房屋的能力。


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#问题


11
作为闭包的示例,该代码是“正确的”,即使在externalFunction返回后未解决使用闭包的注释部分也是如此。因此,这不是一个很好的例子。还有很多其他可以使用闭包的方式,这些方式不涉及返回innerFunction。例如,innerFunction可以传递到另一个函数,在该函数中立即调用它,或者在以后的某个时间存储并调用它,并且在所有情况下,它都可以访问在调用它时创建的externalFunction上下文。
dlaliberte 2011年

6
@syockit不,莫斯错了。无论函数是否转义了定义的范围,都会创建一个闭包,并且无条件创建对父级词法环境的引用会使父级作用域中的所有变量可用于所有函数,无论它们是在外部还是内部调用创建它们的范围。
Asad Saeeduddin

176

通过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)调用时具有的任何值,因此返回的嵌套函数将始终返回调用n时具有的值createClosure(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,都会为该范围内引用它的所有内容更改它。

  • 添加到数组中的所有函数都index从定义它的父作用域返回SAME 变量,而不是像第一个示例那样从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


1
很好,谢谢。为了更清楚一点,我们可以想象一下每次迭代如何在“坏”循环中创建“坏”数组:第一个迭代:[function(){return'n ='+ 0;}]第二个迭代:[( function(){return'n ='+1;}),(function(){return'n ='+1;})]第三次迭代:[(function(){return'n ='+ 2;}) ,(function(){return'n ='+ 2;}),(function(){return'n ='+ 2;})]等。因此,每次索引值更改时,它都会反映在所有函数中已经添加到数组中。
Alex Alexeev

3
使用letvar修复的差异。
Rupam Datta

这里的“关闭正确吗”不是“关闭内部关闭”的示例吗?
TechnicalSmile

我的意思是,每个函数从技术上来说都是一个闭包,但重要的是该函数在其中定义了一个新变量。获取的函数仅返回n在新闭包中创建的引用。我们只是返回一个函数,以便我们可以将其存储在数组中并在以后调用它。
Chev

如果你想只将结果存储在第一次迭代数组中,那么你可以内嵌这样的:arr[index] = (function (n) { return 'n = ' + n; })(index);。但是然后您将结果字符串存储在数组中,而不是将要调用的函数存储在数组中,这违背了我的示例的观点。
Chev

164

关于关闭的维基百科

在计算机科学中,闭包是一个函数,以及对该函数的非本地名称(自由变量)的引用环境。

从技术上讲,在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)并在以后执行,每次调用它都会生成一个数据库函数。每个生成的函数都有其自己的隐藏数据库对象。闭包的另一个用法示例是当我们不返回一个函数,而是一个对象,该对象包含出于不同目的的多个函数,这些函数中的每个函数都可以访问相同的数据。


2
这是JavaScript闭包的最佳解释。应该是选择的答案。其余的娱乐性很强,但是实际上对于实际的JavaScript编码器来说,这实际上是有用的。
geoidesic '18

136

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

128

即使父母走了,孩子们也将永远记住与父母分享的秘密。这就是函数的闭包。

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"

这就是全部。


15
这对我来说是最有意义的解释,因为它并没有假定您掌握大量的专业术语知识。这里最受好评的解释是假设不了解闭包的人对“词法范围”和“执行上下文”等术语有完整而完整的理解-虽然我可以从概念上理解它们,但我不认为我是我应该对它们的细节感到满意,而其中根本没有术语的解释是使闭包最终引起我注意的原因,谢谢。另外,我认为它也可以很简洁地说明范围。
艾玛W

103

我不明白为什么答案在这里这么复杂。

这是一个闭包:

var a = 42;

function b() { return a; }

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


没有理由相信闭包是解决特定问题的复杂设计方法。不,从函数声明的位置(不运行)的角度来看,闭包只是使用来自更高范围的变量。

现在,它允许您执行的操作会更加壮观,请参阅其他答案。


5
这个答案似乎不太可能使人们困惑。传统编程语言中的粗略等价方法可能是在具有私有常量或属性的对象上创建b()作为方法a。在我看来,令人惊讶的是JS作用域对象实际上是a作为属性而不是常量提供的。而且,如果您对其进行了修改,您只会注意到该重要行为,例如return a++;
Jon Coombs 2015年

1
乔恩说的完全正确。在我最终摸索闭包之前,我很难找到实际的例子。是的,floribon创建了一个闭合,但令我不知所措,这绝对不会告诉我任何事情。
Chev

3
这并没有定义闭包是什么,仅是使用一个闭包的示例。它没有解决范围结束时发生的细微差别。我认为在所有范围都存在的情况下,尤其是在全局变量的情况下,没有人对词法作用域有疑问。
Gerard ONeill

91

dlaliberte第一点的示例:

闭包不仅在您返回内部函数时创建。实际上,封闭函数根本不需要返回。您可以改为将内部函数分配给外部作用域中的变量,或将其作为参数传递给可以立即使用的另一个函数。因此,在调用封闭函数时,封闭函数的关闭可能已经存在,因为任何内部函数都可以在调用它后立即对其进行访问。

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

关于可能的歧义的少量澄清。当我说“实际上,封闭函数根本不需要返回”时。我不是说“没有价值”,而是“仍然活跃”。因此该示例没有显示该方面,尽管它显示了将内部函数传递给外部范围的另一种方式。我试图说明的重点是关于创建封闭的时间(用于封闭函数),因为有些人似乎认为封闭函数返回时会发生。需要一个不同的示例来说明在调用函数时创建了闭包。
dlaliberte 2011年

88

闭包是内部函数可以访问其外部函数中的变量的地方。这可能是闭包最简单的单行解释。


35
那只是解释的一半。关于闭包,需要注意的重要一点是,如果在外部函数退出后仍然引用内部函数,则内部函数仍然可以使用外部函数的旧值。
pcorcoran

22
实际上,不是内部函数可用的外部函数的旧,而是内部变量,如果某些函数能够更改它们,则旧变量可能具有新值。
dlaliberte'8

86

我知道已经有很多解决方案,但是我猜想这个小而简单的脚本可以用来说明这个概念:

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

82

您正在睡觉,请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.");

80

Closures的作者很好地解释了闭包,解释了我们为什么需要它们的原因,还解释了LexicalEnvironment,这对于理解闭包是必需的。
这是摘要:

如果访问变量但不是局部变量怎么办?像这儿:

在此处输入图片说明

在这种情况下,解释器在外部LexicalEnvironment对象中找到变量。

该过程包括两个步骤:

  1. 首先,创建函数f时,不会在空白处创建它。当前有一个LexicalEnvironment对象。在上述情况下,它是一个窗口(函数创建时未定义a)。

在此处输入图片说明

创建函数时,它会获得一个名为[[Scope]]的隐藏属性,该属性引用当前的LexicalEnvironment。

在此处输入图片说明

如果读取了变量,但在任何地方都找不到,则会生成错误。

嵌套函数

函数可以彼此嵌套,形成LexicalEnvironments链,也可以称为作用域链。

在此处输入图片说明

因此,函数g可以访问g,a和f。

关闭

外部函数完成后,嵌套函数可能会继续存在:

在此处输入图片说明

标记词法环境:

在此处输入图片说明

如我们所见 this.say是用户对象中的一个属性,因此在用户完成后它仍然存在。

而且,如果您还记得,在this.say创建时,它(作为每个函数)都会得到一个内部引用this.say.[[Scope]]对当前LexicalEnvironment。因此,当前User执行的LexicalEnvironment保留在内存中。User的所有变量也都是其属性,因此也要小心保留它们,而不是像平常一样。

关键是要确保内部函数将来要访问外部变量,它能够这么做。

总结一下:

  1. 内部函数保留对外部LexicalEnvironment的引用。
  2. 即使外部函数完成,内部函数也可以随时从中访问变量。
  3. 浏览器将LexicalEnvironment及其所有属性(变量)保留在内存中,直到有内部函数引用它为止。

这称为关闭。


78

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
}

所有这四个功能都是闭包,因为它们都访问环境。


59

作为一个现年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  */  }

请注意,浏览器将读取/*...*///停止代码。

NAME:您可以随便使用任何单词都可以调用一个函数。示例“ cookMeal”通常将两个单词连接在一起,并在第二个单词开头加一个大写字母-但这不是必需的。它不能有空格,也不能单独是数字。

家长:“括号”或()JavaScript功能工厂门上的信箱或街道上用于向工厂发送信息包的邮政信箱。例如 cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime),有时可能会标记邮箱,在这种情况下,您知道必须提供哪些数据。

大括号:看起来像这样的“括号” {}是我们工厂的有色窗户。从工厂内部可以看到,但是从外部看不到。

上面的长代码示例

我们的代码以单词function开头,因此我们知道它是其中之一!然后,函数的名称会唱歌 -这是我自己对函数含义的描述。然后括号()。括号总是存在于函数中。有时它们是空的,有时它们中包含一些东西(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()函数的工作。


56

好吧,和一个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


49

一个六岁孩子的答案(假设他知道什么是函数,什么是变量以及什么数据):

函数可以返回数据。您可以从函数返回的一种数据是另一种函数。返回该新函数时,创建该函数的函数中使用的所有变量和参数都不会消失。而是,该父函数“关闭”。换句话说,除了返回的功能外,什么都看不到它,也看不到它使用的变量。该新函数具有特殊的功能,可以回顾创建它的函数内部并查看其中的数据。

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

解释它的另一种非常简单的方法是在范围上:

每当您在较大范围内创建较小范围时,较小范围将始终能够看到较大范围中的内容。


49

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

47

也许除了六岁的孩子中最早熟的孩子之外,还有其他一些例子,但有一些例子使我对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的其余变量,您必须摆脱对它们的杰出引用,

//////////

关于闭包要注意的另外两件事。首先,闭包将始终可以访问其包含函数的最后一个值。

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

    innerFunction();
}

outerFunction();

警报:大猩猩

其次,在创建闭包时,它会保留对其所有封闭函数的变量和函数的引用;它没有选择。但是,因此,应该谨慎使用闭包,或者至少要谨慎使用闭包,因为它们可能占用大量内存;在包含函数完成执行后很长一段时间内,许多变量可以保留在内存中。


45

我只是将它们指向Mozilla Closures页面。这是最好,最简洁,最简单的解释是我发现的关于闭包基础知识和实际用法。强烈建议任何学习JavaScript的人使用。

是的,我什至将其推荐给6岁的孩子-如果6岁的孩子正在学习闭包,那么可以随时理解本文中提供的简洁明了的逻辑是合乎逻辑的。


我同意:所说的Mozilla页面特别简单明了。令人惊讶的是,您的帖子没有像其他帖子那样得到广泛的赞赏。
Brice Coustillas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.