JavaScript 闭包是如何工作的?

新手上路,请多包涵

你会如何向了解它们所包含的概念(例如函数、变量等)但不了解闭包本身的人解释 JavaScript 闭包?

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

原文由 e-satis 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 833
2 个回答

闭包是一对:

  1. 一个函数和

  2. 对该函数的外部范围(词法环境)的引用

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

JavaScript 中的每个函数都维护对其外部词法环境的引用。此引用用于配置调用函数时创建的执行上下文。此引用使函数内部的代码能够“看到”函数外部声明的变量,而不管函数何时何地被调用。

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

在下面的代码中, inner 使用调用 foo 时创建的执行上下文的词法环境形成一个闭包, 关闭 变量 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 中,如果你在另一个函数中声明一个函数,那么外部函数的局部变量在从它返回后仍然可以访问。这样,在上面的代码中,在从 foo 返回 _后_,函数对象 inner 仍然可以使用 secret

闭包的使用

当您需要与函数关联的私有状态时,闭包很有用。这是一个非常常见的场景 - 请记住: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 关闭了变量 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>

模块化

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

 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 = () => console.log(x)
 x = x + 1
 return inner
 }

 foo()() // 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 声明的变量被提升。由于引入了 letconst ,这在现代 JavaScript 中的问题要少得多。

在下面的代码中,每次循环时,都会创建一个新的函数 inner ,它关闭 i 。但是因为 var i 被提升到循环之外,所有这些内部函数都关闭了同一个变量,这意味着 i (3) 的最终值被打印了 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 中的闭包就像在函数声明处保留对作用域的引用( 不是 副本),而后者又保留对其外部作用域的引用,依此类推,一直到顶部的全局对象范围链。

  • 声明函数时会创建闭包;此闭包用于在调用函数时配置执行上下文。

  • 每次调用函数时都会创建一组新的局部变量。

链接

原文由 Joel Anair 发布,翻译遵循 CC BY-SA 4.0 许可协议

JavaScript 中的每个函数都维护到其外部词法环境的链接。词法环境是范围内所有名称(例如变量、参数)及其值的映射。

因此,只要您看到 function 关键字,该函数内的代码就可以访问在函数外声明的变量。

 function foo(x) {
  var tmp = 3;

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

  bar(10);
}

foo(2);

This will log 16 because function bar closes over the parameter x and the variable tmp , both of which exist in the lexical environment of外部函数 foo

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

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

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

原文由 Ali 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题