什么是闭包
闭包由两部分组成:
1、一个函数
2、这个函数外部作用域(词法环境)的引用
词法环境是每个执行上下文(栈结构)的一部分,是标识符(即局部变量的名称)和变量的映射。
js中的每个函数都有一个对它外部词法环境的引用。这个引用用来配置执行函数时创建的执行上下文。这个引用使得函数内的代码能“看到”函数外部声明的变量,而不管函数什么时候、在哪被调用。
如果一个函数被一个函数调用,于是被另一个函数调用,那么就创建了一个外部词法环境的引用链,这个链叫做作用域链。
下面代码中,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不能从外部foo中直接得到
f() // 查找到secret的唯一方式是执行f
换句话说:在js中,函数携带了一个私有“状态集”(box of state)的引用,这个状态集的引用只有这些函数(以及其他在相同词法环境中声明的函数)能够访问。这个状态集对函数调用者来说是不可见的,它提供了一个绝妙的数据隐藏和封装的机制。
记住:js中的函数能像变量(一级函数)一样被传递,这意味着这些功能和状态的组合能在你的程序中传递,类似于在c++中传递一个类的实例。
如果js没有闭包,那么将不得不在函数间显示传递更多的状态,这会让参数列表变得更长,代码更冗杂。
所以,如果你想要一个函数总能访问一些私有状态,你可以使用闭包。
在很多时候我们确实想要给一个函数关联状态。例如,在java或c++中,当你给一个类添加一个私有实例变量(非静态变量)和一个方法时,你就是在给功能关联状态。
在c和大多数常用语言中,一个函数返回后,所有局部变量将不能再访问因为栈结构被销毁了。在js中,如果你在一个函数中声明一个函数,那么它返回后外面函数的局部变量仍能被访问。这样的话,在上面的代码中,在inner被foo返回后,secret对函数对象inner来说仍是有效的。
闭包的用法
当你需要给一个函数关联状态时,闭包就是有用的。这是个很常见的场景,要记得:js直到2015才有了class,并且现在仍然没有私有域。闭包满足了这个需求。
私有实例变量
下面代码中,函数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.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
下面代码中,三个方法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() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42
例3
当你使用var声明的变量时,你要确保你知道包含的是哪个变量。var声明的变量被提升。最新的js中由于let和const的引入这不再是什么问题。
下面代码中,每次循环会创建一个新的包含i的函数inner。但是由于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]()
}
最后的要点:
- 在js中声明函数时都会创建闭包。
- 在函数里返回一个函数是闭包的典型例子,因为外层函数里的状态对返回的内层函数说是隐式可用的,即使外层函数已经执行完了。
- 当你在一个函数里使用eval()时,你就使用了闭包。eval里的文本能引用函数里的局部变量。在非严格模式下,你甚至能通过eval('var foo = ...')创建新的局部变量。
- 当你在函数中使用new Function()时,它不包含这个词法环境:相反它包含的是全局上下文。这个新函数不能引用外部函数的局部变量。
- js中的闭包持有函数声明处的作用域的引用(不是复制),这使得它也持有它外层作用域的引用,以此类推,直到作用域链的顶端——全局对象。
- 闭包在函数声明时创建;闭包被用来配置函数执行时的执行上下文。
- 每次调用函数时会创建一系列新的局部变量。