定义
《JavaScript高级程序设计》对闭包的定义:
闭包指那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
e.g.:
function foo () {
a = 1
return function () {
console.log(a)
}
}
var bar = foo()
bar() // 1
思考
Q1:我们知道,当调用闭包的外部函数的时候,在函数执行结束之后,函数的执行上下文就会退出执行上下文栈,活动对象的也会被销毁。那为什么在外部函数被调用之后,再调用闭包的内部函数的时候还能够访问外部函数中的变量呢?例如,在上面例子中, var bar = foo() 执行结束之后,foo() 的变量对象已经被销毁了,为什么 bar() 执行的时候还能够访问到 foo() 中的变量 a 呢?
A1:这是因为作用域链的存在。回想一下一个函数从创建到执行的过程:
-
函数声明后,设置函数的 [[scope]] 内部属性,其中包含了该函数各级父级执行上下文的变量对象
-
函数被调用之后,创建一个执行上下文,并将这个执行上下文压入执行上下文栈中
-
复制函数的 [[scope]] 内部属性,创建作用域链
-
通过 arguments 创建活动对象并初始化
-
将活动对象添加到作用域链最前端
-
准备工作完成,执行函数
-
函数执行完毕,执行上下文出栈,销毁活动对象
从函数创建到执行的整个过程中,我们注意到:1. 在函数声明之后,函数的 [[scope]] 属性就已经保存了各级父级执行上下文的变量对象 2. 在函数调用的时候,复制 [[scope]] 属性创建作用域链。因此,在外部函数执行之后,即使外部函数的活动对象已经被销毁了,但是在闭包函数执行的时候,由于作用域链中已经保存了外部函数的变量对象,闭包函数还是能够沿着作用域链找到外部函数作用域中的变量。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
Q2:分析上面这段代码,为什么 data[0]()…全都输出 3 ?
A2:在执行 data[i]() 的时候,由于它们的作用域中都没有定义变量 i,因此执行 console.log(i) 的时候,就会沿着作用域链搜索各级父级作用域。而 data[i]() 它们的上级作用域都是全局作用域,通过 var 声明的变量 i 就定义在全局作用域中。因此,1. data[i]() 打印的 i 都是同一个值——全局作用域中的 i 。另外,2. 在data[i]() 被调用之前,因为 for 循环结束之后,i 的值已经变成了 3,data[i]() 的 [[scope]] 属性中保存的 i 也同时被更改了(个人理解),因此最终打印的都是 3.
Q3:如何修改上面的代码,让 data[i]() 输入对应的 i ?
A3:利用闭包的原理,只需要在 data[i]() 的作用域与全局作用域之间各自加一层父级作用域,在各个父级作用域中保存各个 i 的值,这样它们在执行的时候沿着作用域链搜索到各自的父级作用域就找到 i 了,就不会共享全局作用域中的变量了。闭包通常通过嵌套函数实现,在上面代码中则可以通过 IIFE 实现。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function () {
console.log(i)
})(i); // IIFE 实现闭包
}
注意点
闭包函数中的 this 和 arguments
-
和外部函数作用域中的变量不同,闭包函数在调用的时候不能直接访问外部函数的 this 和 arguments。因为在外部函数执行结束后,执行上下文(包括this和通过 arguments 作用域链)已经退出执行上下文栈了,而且没有保存到闭包函数中(个人理解)。因此,闭包函数中的 this 会指向 window(严格模式下为 undefined)
var obj = { getThis () { console.log(this) return function () { console.log(this) } } } var bar = obj.getThis() // {getThis: f} bar() // Window{...}
-
虽然不能在闭包函数中直接访问外部函数的 this 和 arguments,但是如果把 this 和 arguments 保存到外部函数的一个变量中,闭包函数则可以通过这个变量访问外部函数的 this 和 arguments(作用域链)
var obj = { getThis () { console.log(this) var that = this return function () { console.log(that) } } } var bar = obj.getThis() // {getThis: f} bar() // {getThis: f}
闭包的性能问题
《JavaScript高级程序设计》:
因为闭包会保留它们包含函数的作用域,因此比其他函数更占用内存。过度适用闭包可能导致内存占用过度,因此建议仅在十分必要时使用。V8等优化的JavaScript引擎会努力回收被闭包困住的内存,但是还是建议谨慎只用闭包。