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

我在维基百科上看到了the Scheme example篇文章,但不幸的是它没有帮助.

推荐答案

闭包是一对:

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

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

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

如果一个函数被一个函数调用,而另一个函数又被另一个函数调用,那么就会创建一个指向外部词汇环境的引用链.这个链称为范围链.

在下面的代码中,inner与调用foo时创建的执行上下文的词汇环境(closing over变量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没有闭包,那么就必须在函数explicitly之间传递更多的状态,从而使参数列表更长,代码噪音更大.

所以,如果你想让一个函数总是能够访问一个私有的状态,你可以使用闭包.

...我们经常想把状态和函数联系起来.例如,在java或C++中,当向类添加私有实例变量和方法时,您将状态与功能关联起来.

在C和大多数其他常见语言中,函数返回后,由于堆栈框架被 destruct ,所有局部变量都不再可访问.在JavaScript中,如果在另一个函数中声明一个函数,那么外部函数的局部变量在返回后仍然可以访问.这样,在上面的代码中,secret对函数对象inner保持可用,afterfoo返回.

闭包的使用

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

模块化

在下面的示例中,所有实现细节都隐藏在立即执行的函数表达式中.功能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

这个例子表明局部变量没有复制到闭包中:闭包保持对原始变量themselves的引用.这就好像即使在外部函数退出后,堆栈帧仍在内存中保持活动状态.

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)的最终值被打印三次.

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是闭包的classic 示例,因为外部函数内部的状态对返回的内部函数隐式可用,即使在外部函数完成执行之后也是如此.
  • 每当在函数中使用eval()时,就会使用闭包.文本eval可以引用函数的局部变量,在非严格模式下,甚至可以使用eval('var foo = …')创建新的局部变量.
  • 当你在一个函数中使用new Function(…)(Function constructor)时,它不会在词法环境中关闭:而是在全局上下文中关闭.新函数不能引用外部函数的局部变量.
  • JavaScript中的闭包类似于在函数声明点处保持对作用域的引用(NOT个副本),进而保持对其外部作用域的引用,依此类推,一直到作用域链顶部的全局对象.
  • 在声明函数时创建闭包;该闭包用于在调用函数时配置执行上下文.
  • 每次调用函数时,都会创建一组新的局部变量.

链接

Javascript相关问答推荐

如何才能拥有在jQuery终端中执行命令的链接?

窗口.getComputedStyle()在MutationObserver中不起作用

node TS:JWT令牌签名以验证客户端和后台问题之间的身份验证

Google Apps脚本中的discord邀请API响应的日期解析问题

有没有可能使滑动img动画以更快的速度连续?

将旋转的矩形zoom 到覆盖它所需的最小尺寸&S容器

更改预请求脚本中重用的JSON主体变量- Postman

扩展类型的联合被解析为基类型

如何在Node.js中排除导出的JS文件

为什么我的按钮没有从&q;1更改为&q;X&q;?

使用插件构建包含chart.js提供程序的Angular 库?

为什么云存储中的文件不能公开使用?

将基元传递给THEN处理程序

如何用javascript更改元素的宽度和高度?

在JS/TS中将复杂图形转换为数组或其他数据 struct

Cherrio JS返回父div的所有图像SRC

有没有办法通过使用不同数组中的值进行排序

Chart.js Hover线条在鼠标离开时不会消失

Plotly.js栏为x轴栏添加辅助文本

Firefox的绝对定位没有达到预期效果