JavaScript 闭包是如何工作的? [英] How do JavaScript closures work?

查看:25
本文介绍了JavaScript 闭包是如何工作的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

How would you explain JavaScript closures to someone with a knowledge of the concepts they consist of (for example functions, variables and the like), but does not understand closures themselves?

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

I have seen the Scheme example given on Wikipedia, but unfortunately it did not help.

推荐答案

闭包是一对:

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

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

A lexical environment is part of every execution context (stack frame) and is a map between identifiers (ie. local variable names) and values.

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

Every function in JavaScript maintains a reference to its outer lexical environment. This reference is used to configure the execution context created when a function is invoked. This reference enables code inside the function to "see" variables declared outside the function, regardless of when and where the function is called.

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

If a function was called by a function, which in turn was called by another function, then a chain of references to outer lexical environments is created. This chain is called the scope chain.

在下面的代码中,inner与调用foo时创建的执行上下文的词法环境形成一个闭包,关闭变量秘密:

In the following code, inner forms a closure with the lexical environment of the execution context created when foo is invoked, closing over variable 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 中,函数携带对私有状态框"的引用,只有它们(以及在同一词法环境中声明的任何其他函数)才能访问该状态框".这个状态框对于函数的调用者是不可见的,提供了一个很好的数据隐藏和封装机制.

In other words: in JavaScript, functions carry a reference to a private "box of state", to which only they (and any other functions declared within the same lexical environment) have access. This box of the state is invisible to the caller of the function, delivering an excellent mechanism for data-hiding and encapsulation.

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

And remember: functions in JavaScript can be passed around like variables (first-class functions), meaning these pairings of functionality and state can be passed around your program: similar to how you might pass an instance of a class around in C++.

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

If JavaScript did not have closures, then more states would have to be passed between functions explicitly, making parameter lists longer and code noisier.

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

So, if you want a function to always have access to a private piece of state, you can use a closure.

...而且我们经常确实希望将状态与函数相关联.例如,在 Java 或 C++ 中,当您向类添加私有实例变量和方法时,您将状态与功能相关联.

...and frequently we do want to associate the state with a function. For example, in Java or C++, when you add a private instance variable and a method to a class, you are associating state with functionality.

在 C 和大多数其他常见语言中,在函数返回后,所有局部变量都不再可访问,因为堆栈帧被破坏.在 JavaScript 中,如果你在另一个函数中声明一个函数,那么外部函数的局部变量在从它返回后仍然可以访问.这样,在上面的代码中,secret 对函数对象inner 仍然可用,之后foo<返回/代码>.

In C and most other common languages, after a function returns, all the local variables are no longer accessible because the stack-frame is destroyed. In JavaScript, if you declare a function within another function, then the local variables of the outer function can remain accessible after returning from it. In this way, in the code above, secret remains available to the function object inner, after it has been returned from foo.

当您需要与函数关联的私有状态时,闭包很有用.这是一个非常常见的场景 - 请记住:JavaScript 直到 2015 年才有类语法,而且它仍然没有私有字段语法.闭包满足了这种需求.

Closures are useful whenever you need a private state associated with a function. This is a very common scenario - and remember: JavaScript did not have a class syntax until 2015, and it still does not have a private field syntax. Closures meet this need.

在下面的代码中,函数toString关闭了汽车的细节.

In the following code, the function toString closes over the details of the car.

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.

In the following code, the function inner closes over both fn and args.

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.

In the following code, function onClick closes over 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>

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

In the following example, all the implementation details are hidden inside an immediately executed function expression. The functions tick and toString close over the private state and functions they need to complete their work. Closures have enabled us to modularise and encapsulate our code.

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

这个例子表明局部变量没有被复制到闭包中:闭包维护一个对原始变量他们自己的引用.就好像堆栈帧在外部函数退出后仍然存在于内存中一样.

This example shows that the local variables are not copied in the closure: the closure maintains a reference to the original variables themselves. It is as though the stack-frame stays alive in memory even after the outer function exits.

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

在下面的代码中,logincrementupdate 三个方法都关闭在同一个词法环境中.

In the following code, three methods log, increment, and update all close over the same lexical environment.

并且每次 createObject 被调用时,都会创建一个新的执行上下文(堆栈帧)和一个全新的变量 x,以及一组新的函数(log 等)被创建,关闭这个新变量.

And every time createObject is called, a new execution context (stack frame) is created and a completely new variable x, and a new set of functions (log etc.) are created, that close over this new variable.

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

如果您使用的是使用 var 声明的变量,请注意您了解要关闭的变量.使用 var 声明的变量被提升.由于引入了 letconst,这在现代 JavaScript 中的问题要小得多.

If you are using variables declared using var, be careful you understand which variable you are closing over. Variables declared using var are hoisted. This is much less of a problem in modern JavaScript due to the introduction of let and const.

在下面的代码中,每次循环时,都会创建一个新函数inner,它关闭i.但是因为 var i 被提升到循环之外,所有这些内部函数都关闭了同一个变量,这意味着 i (3) 的最终值被打印了 3 次.

In the following code, each time around the loop, a new function inner is created, which closes over i. But because var i is hoisted outside the loop, all of these inner functions close over the same variable, meaning that the final value of i (3) is printed, three times.

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 中的闭包就像在函数声明点保留对作用域的引用(不是副本),而后者又保留对其外部作用域的引用,等等,所有作用域链顶部的全局对象的路径.
  • 在声明函数时创建一个闭包;这个闭包用于在调用函数时配置执行上下文.
  • 每次调用函数时都会创建一组新的局部变量.
  • Whenever a function is declared in JavaScript closure is created.
  • Returning a function from inside another function is the classic example of closure, because the state inside the outer function is implicitly available to the returned inner function, even after the outer function has completed execution.
  • Whenever you use eval() inside a function, a closure is used. The text you eval can reference local variables of the function, and in the non-strict mode, you can even create new local variables by using eval('var foo = …').
  • When you use new Function(…) (the Function constructor) inside a function, it does not close over its lexical environment: it closes over the global context instead. The new function cannot reference the local variables of the outer function.
  • A closure in JavaScript is like keeping a reference (NOT a copy) to the scope at the point of function declaration, which in turn keeps a reference to its outer scope, and so on, all the way to the global object at the top of the scope chain.
  • A closure is created when a function is declared; this closure is used to configure the execution context when the function is invoked.
  • A new set of local variables is created every time a function is called.
  • Douglas Crockford's simulated private attributes and private methods for an object, using closures.
  • A great explanation of how closures can cause memory leaks in IE if you are not careful.
  • MDN documentation on JavaScript Closures.

这篇关于JavaScript 闭包是如何工作的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆