闭包是一对:
- 一个函数,和
- 对该函数外部作用域的引用(词法环境)
词法环境是每个执行上下文(堆栈帧)的一部分,是标识符(即局部变量名称)和值之间的映射。
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 中,如果你在另一个函数中声明一个函数,那么外部函数的局部变量在从它返回后仍然可以访问。这样一来,在上面的代码,
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在
fn和 上关闭
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。
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.countercounter.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
在下面的代码中,三个方法
log、
increment和
update都关闭在同一个词法环境中。
每次
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() // 43o.update(5)o.log() // 5const p = createObject()p.log() // 42示例 3
如果您正在使用 using 声明的变量
var,请注意您了解要关闭的变量。使用声明的变量
var被提升。由于引入了
let和,这在现代 Javascript 中的问题要小得多
const。
在下面的代码中,每次循环时,
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
从另一个函数内部返回 a是闭包的经典示例,因为外部函数内部的状态对于返回的内部函数是隐式可用的,即使外部函数已完成执行。- 每当您
eval()
在函数内部使用时,都会使用闭包。文本eval
可以引用函数的局部变量,在非严格模式下,甚至可以使用eval('var foo = …'). - 当您
new Function(…)
在函数内使用(Function 构造函数 时,它不会关闭其词法环境:而是关闭全局上下文。新函数不能引用外部函数的局部变量。 - Javascript 中的闭包就像在函数声明点保留对作用域的引用(而不是副本),而后者又保留对其外部作用域的引用,依此类推,一直到顶部的全局对象作用域链。
- 声明函数时会创建一个闭包;这个闭包用于在调用函数时配置执行上下文。
- 每次调用函数时都会创建一组新的局部变量。



