JavaScript 闭包是如何工作的?

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

我在维基百科上看到了Scheme的例子,但不幸的是它没有帮助。


答案 1

闭包是以下各项的配对:

  1. 函数和
  2. 对该函数的外部作用域(词法环境)的引用

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

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

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

在下面的代码中,使用调用时创建的执行上下文的词法环境形成一个闭包,在变量上关闭innerfoosecret

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 中,如果您在另一个函数中声明一个函数,则外部函数的局部变量在从该函数返回后仍可访问。这样,在上面的代码中,在函数对象从 返回,它仍然对函数对象可用。secretinnerfoo

瓶盖的用途

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

函数式编程

在下面的代码中,函数在 和 上关闭。innerfnargs

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

面向事件的编程

在下面的代码中,函数在变量 上关闭。onClickBACKGROUND_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

每次调用时,都会创建一个新的执行上下文(堆栈帧),并创建一个全新的变量,并创建一组新的函数(等),这些函数(等)关闭了这个新变量。createObjectxlog

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

如果使用 的声明变量,请注意了解要关闭的变量。使用 声明的变量被吊装。这在现代 JavaScript 中不是一个问题,因为 引入了 和 。varvarletconst

在下面的代码中,每次循环期间,都会创建一个新函数,该函数在 上关闭。但是由于被吊起在循环之外,所有这些内部函数都在同一变量上关闭,这意味着(3)的最终值被打印三次。innerivar ii

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()evaleval('var foo = …')
  • 在函数中使用(Function 构造函数)时,它不会在其词法环境中关闭:而是在全局上下文中关闭。新函数不能引用外部函数的局部变量。new Function(…)
  • JavaScript 中的闭包就像在函数声明点保留对作用域的引用(不是副本),这反过来又保留对其外部作用域的引用,依此类推,一直到作用域链顶部的全局对象。
  • 在声明函数时创建闭包;此闭包用于在调用函数时配置执行上下文。
  • 每次调用函数时都会创建一组新的局部变量。

链接


答案 2

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

因此,每当您看到关键字时,该函数内的代码都可以访问在函数外部声明的变量。function

function foo(x) {
  var tmp = 3;

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

  bar(10);
}

foo(2);

这将记录,因为函数在参数和变量上关闭,两者都存在于外部函数的词法环境中。16barxtmpfoo

函数,连同它与函数的词法环境的联系是一个闭包。barfoo

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

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,因为里面的代码仍然可以引用参数和变量,即使它们不再直接在作用域中。barxtmp

但是,由于仍然在闭合内部徘徊,因此可以递增。每次调用 时,它都会递增。tmpbarbar

最简单的闭包示例是这样的:

var a = 10;

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

调用 JavaScript 函数时,将创建一个新的执行上下文。与函数参数和目标对象一起,此执行上下文还接收指向调用执行上下文的词法环境的链接,这意味着在外部词法环境中声明的变量(在上面的示例中,和 )都可以从 中获得。ecabec

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

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