本文章著作权归饥人谷_Lyndon和饥人谷所有,转载请注明出处。
闭包对于我而言是一个难点,但闭包又是一个很有用的知识点,很多高级应用都需要依赖闭包。
所以在参考一些文章加上大量练习后,我来写一写自己理解闭包的过程,首先是弄清楚以下几个知识点。
>>> Part 1. 变量的作用域
JS中,变量的作用域只有两种:全局作用域、函数作用域。对应的变量也只有两种:全局变量、局部变量。
函数内部可以直接读取全局变量。
var a = 1;
function f(){
console.log(a);
}
f(); // 1
但是函数外部无法读取到函数内部的局部变量。
function f(){
var a = 1;
}
console.log(a); // Uncaught ReferenceError: a is not defined
这一个Part是比较好理解的。
>>> Part 2. 如何从外部读取到局部变量?
在禄永老师的公开课中,老师将从外部读取局部变量这一情况称作“伟大的逃脱”。总结而言,有两种方法来实现。
- 返回值的方法:函数作为返回值
function f1(){
var a = 1;
function f2(){
console.log(a);
}
return f2;
}
var result = f1();
result(); // 1
函数f2包裹在函数f1内,根据作用域链的原理:子对象会一级一级向上寻找父对象的变量,f1所有的局部变量都可以被f2访问到,反之则不行。因此只要把f2作为返回值,就可以在f1外部读取到其中的内部变量。
- 句柄的方法:定义全局变量
var innerHandler = null;
function outerFunc(){
var outerVar = 1;
function innerFunc(){
console.log(outerVar);
var innerVar = 2;
}
innerHandler = innerFunc;
}
outerFunc();
innerHandler(); // 1
这一方法首先定义了一个值为null
的全局变量innerHandler
,然后让innerHandler
等于函数内部的函数,函数内部的函数则可以通过作用域链访问到父对象的变量outerVar
,之后在外部调用innerHandler
的时候,就可以访问到outerFunc
函数中的内部变量outerVar
。
>>> Part 3. 闭包
网络上有千万种对闭包的解释,其实闭包就是上面例子中的两个函数:f2以及innerFunc。书面解释就是:能够读取其他函数内部变量的函数。
在JS中,因为父函数内部的子函数才能够读取局部变量,因此闭包的常见形式就是:定义在函数内部的函数。前者是后者的充分不必要条件。
一言以蔽之,闭包就是连接函数内部外部的渠道。
>>> Part 4. 对示例代码段的解答
- 第一段代码
function outerFn() {
console.log("Outer function");
function innerFn() {
var innerVar = 0;
innerVar++;
console.log("Inner function\t");
console.log("innerVar = "+innerVar+"");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
在这一段代码当中,innerFn
不是一个闭包,因为它并不需要读取其他函数的内部变量,唯一的变量innerVar
就在innerFn
函数内部。在第一个fnRef()
之后,结果就是首先输出Outer function
,然后输出Inner function
,由于innerVar
是函数innerFn
的内部变量且自增,因此从0变为1,再输出innerVar = 1
.
这时候需要明白,当再次运行fnRef()
时,由于fnRef
本身已经变成了函数innerFn
,所以其输出结果就不再有Outer Function
这一句,而是直接输出:Inner function
以及innerVar = 1
.原因是此时的innerVar
是一个内部变量,其作用域限定在innerFn
函数中,每次调用执行innerFn
函数,innerVar
都会被重写。
对于下面的fnRef2()
,也是同理。最后的输出结果见下图:
- 第二段代码
var globalVar = 0;
function outerFn() {
console.log("Outer function");
function innerFn() {
globalVar++;
console.log("Inner function\t");
console.log("globalVar = " + globalVar + "");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
这里的globalVar
是一个外部变量,也是一个全局变量,处于全局作用域下。所以当执行innerFn
时,innerFn
函数将会访问到一个每次都自增的全局作用域下的活动对象,因此输出的结果会从globalVar = 1
一直到globalVar = 4
.在执行间歇中,globalVar
处于两个函数的作用域之外,天高地远谁也管不了,所以它的值会被保存在内存中,并不会立刻被抹去。最后的输出结果见下图:
- 第三段代码
function outerFn() {
var outerVar = 0;
console.log("Outer function");
function innerFn() {
outerVar++;
console.log("Inner function\t");
console.log("outerVar = " + outerVar + "");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
闭包来临了,这里的fnRef
是一个闭包innerFn
函数,但是此时的变量outerVar
来到了父函数的作用域内,不像之前一样处于子函数作用域内或者处于全局作用域下。可以发现,这和Part 2中的例子非常相似。
其原理是:外部函数的调用环境为相互独立的封闭闭包的环境,第二次的fnRef2
调用outerFn
没有沿用第一次调用fnRef
时outerVar
的值,第二次函数调用的作用域创建并绑定了一个新的outerVar
实例,两个闭包环境中的计数器是相互独立,不存在关联的。
进一步来说,在每个封闭闭包环境中,外部函数的局部变量会保存在内存中,并不会在外部函数调用后被自动清除。原因在于:outerFn
是innerFn
的父函数,而innerFn
被赋值给一个全局变量,因此innerFn
始终在内存当中,而它又依赖于outerFn
,所以outerFn
也必须始终在内存中,不会再函数被调用后就被抹去,因此闭包也有一点点不好,有可能造成内存泄漏。
所以,结果应该是:outerVar = 1
, outerVar = 2
, outerVar = 1
, outerVar = 2
.结果如下图所示:
我写到这自己已经完全明白了,我现在要用自己的理解来理顺一下最经典的问题。
>>> Part 5. 理顺最经典问题
0
1
2
3
最经典的问题是:为什么我点击任何数字,控制台的输出结果永远是4?
这里可使用作用域链来帮助理解,不妨将以上代码转化为:
// function只是传递给了NodeList类型对象中的元素却并未执行,因为后面无括号
spans[0] = function fn0(){console.log(i)};
spans[1] = function fn1(){console.log(i)};
spans[2] = function fn2(){console.log(i)};
spans[3] = function fn3(){console.log(i)};
globalContext = {
AO: {
i: undefined, // 0(fn0)1(fn1)2(fn2)3(fn3)4(终止循环)
spans:[0], [1], [2], [3]
},
scope: null
}
fn0[[scope]] = globalContext.AO,
fn1[[scope]] = globalContext.AO,
fn2[[scope]] = globalContext.AO,
fn3[[scope]] = globalContext.AO
fn0Context = {
AO:{
},
scope: fn0[[scope]]
}
fn1Context = {
AO:{
},
scope: fn1[[scope]]
}
fn2Context = {
AO:{
},
scope: fn2[[scope]]
}
fn3Context = {
AO:{
},
scope: fn3[[scope]]
}
最后点击span元素的时候i早已变为4,因此永远输出4.
改进的方法可以使用闭包,也就是:
var spans = document.querySelectorAll("#divTest span");
for(var i = 0; i < spans.length; i++) {
spans[i].onclick = function(i){
return function (){
console.log(i);
}
}(i);
}
这个闭包也可以用作用域链来理解:
globalContext = {
AO:{
i: undefined,
spans: [0], [1], [2], [3]
}
}
fn0.scope = globalContext.AO,
fn1.scope = globalContext.AO,
fn2.scope = globalContext.AO,
fn3.scope = globalContext.AO
fn0Context = {
AO:{
i: 0,
function: anonymous
}
fn0[[scope]] = fn0.scope // globalContext.AO
}
function_anonymousContext = {
AO: {
}
function_anonymous[[scope]] = fn0Context.AO
}
...
>>> Part 6. 闭包的问题
如同刚才的分析一样,当涉及到闭包时,函数中的变量都会被保存在内存中,因此需要避免滥用闭包,否则就有可能导致内存泄露。
>>> 参考资料
- 阮一峰:学习Javascript闭包(Closure)
- 认识闭包-禄永老师
- segmentfault: for因闭包引起的实际问题
- Stack Overflow: How do JavaScript closures work?