闭包的简单理解:函数A在函数B内容进行了定义,当函数A在执行时需要访问函数B中的变量对象,那么函数B就是一个闭包。简单看一个小例子,大概就能简单理解何为闭包。1
2
3
4
5
6
7
8function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
对于上述代码,很好理解,就是调用foo()方法,其中bar()嵌套在foo()中,并且bar()方法体中要在控制台输出a的值,而这个a的声明在bar()外,相对于bar()来说a是一个全局变量,这样进行RHS查询,可以得到a值为2,上面这个例子可以对闭包进行一个简单的理解,函数bar()在函数foo()内容进行了定义,当函数A在执行时需要访问函数foo()中的变量对象,那么函数foo()就是一个闭包。这个小例子其实不能突出的展示出闭包的作用,在《你不知道的JavaScript》一书中,举出了另一个例子,可以很好地展示出闭包的真正作用,清晰地展示了闭包。1
2
3
4
5
6
7
8
9function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 ———— 朋友,这就是闭包的效果。
上面例子中,发现跟第一个例子区别在于bar被return了,调用foo()方法将他赋值给变量baz,这时候其实baz被赋予的就是bar()方法,而JavaScript存在垃圾回收机制,foo()被执行调用后,应该整个内部作用域都会被销毁,但由于foo()是一个闭包,所以闭包阻止了这样的事发生,因为就算自身不用了,内部函数方法bar()还是需要访问foo()中的变量对象,所以foo()并没有被回收。
闭包的常用场景之一是setTimeout函数。该函数可以做到延迟执行,观察下面的例子:1
2
3setTimeout( function timer() {
console.log( 'hello' );
}, 1000 );
上述例子理解起来很简单,就是一秒后输出字符串hello,控制台可以将此代码复制进去,看看结果,第一行出现的值是调用该函数返回的一个唯一Id。结合setTimeout与循环,也可以与闭包很好的联系起来,看一下下面的例子:1
2
3
4
5for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
这个循环例子输出结果可能有部分人认为输出结果应该是1-5,但事实是每秒输出的值都为6。其实可以通过很多种方法解决这种问题,比如闭包,但这里我简单解释一下为什么会输出的都是6?我们先简单看一下例子:1
2
3
4
5
6
7
8
9
10
11
12//Demo1.js
setTimeout(()=>{
console.log("setTimeout");
},0);
console.log("not setTimeout");
//Demo2.js
let start = new Date();
setTimeout(()=>{
console.log(new Date() - start);
}, 500)
while (new Date() - start <= 1000) {}
对于Demo1,部分人看到setTimeout中的延时毫秒数为0,那应该是首先输出字符串setTimeout,但结果肯定是相反的,因为setTimeout存在一个最小的执行时间,当指定时间小于最小执行时间时,默认用最小执行时间,所以就算延迟毫秒数为0,默认最小执行时间大于0,所以字符串setTimeout会在最后输出。对于Demo2,我第一眼看到这个例子的时候,以为输出值应该为500,但事实并不是这样的,为什么呢?因为下面有个循环判断语句,只有当当前时间与定义时间毫秒数小于等于1000时,才跳出循环,这样才轮到setTimeout执行里面的方法,输出结果都是比1000大的值。通过上面两个小例子,发现setTimeout延迟函数的执行都会等到其他函数调用栈清空后才会执行,这时候未执行的setTimeout函数会放在队列结构中,等到其他函数调用都结束后,setTimeout遵循先进先出规则,依次输出结果。所以对于刚开始的例子为什么输出结果都为6,相信应该大概知道啥回事了。就是当for(…)循环执行完毕后,也就是当i=6时,循环结束,这时候依次从队列中输出setTimeout输出的值,也就是5次6。
那应该怎样利用闭包对上述循环的例子进行改进呢?其实很简单,只要我们每次把i的值存下来就可以了,具体实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 1
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
// 2
for (var i=1; i<=5; i++) {
let j = i; // 是的,闭包的块作用域!
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}
闭包的另一个强大的使用场景就是模块。《你不知道的JavaScript》一书中给出的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
书中引用:“模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至 少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包”。
参考资料:
详细图解作用域与闭包
你应该知道的setTimeout秘密