认识闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的。1
2
3
4
5
6
7
8
9function foo(){
var a = 2;
function bar(){
console.log( a );
}
return bar;
}
var baz = foo();//此时 baz = bar;
baz();//2
在 foo() 执行后,其返回值(就是 bar 函数)赋给了变量 baz。
然后在调用 baz(),实际调用的是 bar()。
bar() 的词法作用域是在 foo() 的内部作用域,而调用执行的位置是在 foo() 的外部作用域。
在 foo()执行后,其作用域原本是应该被回收的,但是依然存在没有被回收,是因为 bar() 还在使用。
bar()依旧对这个作用域可以引用,这个引用就叫做闭包。
这个不是闭包
1 | var a = 2; |
这个不是闭包,根据闭包的概念,它不是在本身的词法作用域外执行的,它是在定义时所在的作用域种执行。a 是通过普通的此法作用域查找的,而不是闭包发现的。
使用了回调函数就是使用闭包
1 | function wait(message){ |
注意:setTimeout()方法一定要传入一个函数的。
结果就是在 wait() 执行1000毫秒疑惑,它的内部作用域不会消失, timer 函数依然保有 wait()作用域的闭包。
在引擎内部,内置的工具函数 setTimeout() 持有对一个参数的引用,引擎会调用这个函数,在例子中就是内部的 timer 函数,并且词法作用域在这个过程保持完整,这就是闭包。
在定时器、事件监听器、Ajax 请求等异步(或者同步)任务中,只要使用了回调函数,实际上就是使用闭包。
在循环里的闭包
1 | for(var i=1; i<=5; i++){ |
预想是分别输出数字1-5,每秒一次,每次一个。
结果是以每秒一次的频率输出五次6。
因为延迟函数(就是 setTimeout)会在循环结束后才开始执行,循环结束的条件是 i=6,并且 i 的作用域是全局的。
循环结束后,延迟函数会执行五次,并且执行的是五次闭包(即 timer()),虽然这五次函数是在各个迭代种分别定义的,但是它们都被封闭在一个共享的全局作用域种,因此实际上只有一个 i,并且 i=6。
如果想得到预想结果,我们需要改进一下代码。
有自己的变量,用在每个迭代中存储 i 的值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14for(var i=1; i<=5; i++){
(function(){ //这里创造了一个内部的作用域,当执行五次闭包时,它的作用域在这里,并且变量 j 也被储存下来了。
var j = i;
setTimeout( function timer(){
console.log(j);},j*1000);
})();
}
//进一步改善
for(var i=1; i<=5; i++){
(function(j){
setTimeout( function timer(){
console.log(j);},j*1000);
})(i);
}引入块作用域
使用 let 关键字(可以将变量绑定到所在的任意作用域种(通常是{…}内部)),引入块作用域。1
2
3
4
5
6
7
8
9
10
11for( var i=1; i<=5; i++){//这里创建块作用域。
let j = i; //注意是 let 而不是 var,否则还是全局作用域。
setTimeout( function timer(){
console.log(j);},j*1000);
}
//或者
for( let i=1; i<=5; i++)//圆括号这里创建块作用域。
{
setTimeout( function timer(){
console.log(j);},j*1000);
}
总之,加入块作用域或者新的作用域(函数作用域等),使用闭包就十分方便。