认识闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz(); //2
函数bar()的词法作用域能够访问foo()的内部作用域,在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,但是闭包可以阻止其发生,因为bar()仍然在使用内部作用域。
bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
function foo(){
var a = 2;
function bar(){
console.log(a);
}
bar(baz);
}
function bar(fn){
fn();
}
这个例子中,内部函数baz传递给bar,当调用这个内部函数时,它涵盖的foo()内部作用域的闭包就可以观察到了,因为它能够访问a。
间接传递函数
var fn;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
fn = baz;
}
function bar(){
fn();
}
foo();
bar();//2
总结:上述产生闭包的三种手段:1.通过return返回内部函数;2.将内部函数当做实参传递;3.将内部函数赋值到另一个变量中
循环和闭包
想一想下面这段代码
for(var i = 1; i <=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}
这段代码在运行时会以每秒一次的频率输出五次6。
这是为什么呢?
实际上,当定时器运行时即使每个迭代中执行的是setTimeout(),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6出来。
这里的缺陷是我们试图假设循环中每个迭代在运行时都会给自己“捕获”一个i的副本。但是实际情况却是,五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际只有一个i。所以我们需要更多的闭包作用域,特别是在循环过程中每个迭代都需要一个闭包作用域。
我们可以对这段代码进行改进
for(var i = 1; i <=5; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},j*1000);
})(i);
}
在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们使用。
使用let劫持块作用域
function(var i = 1; i <=5 ;i++){
let j = i;
setTimeout(function timer(){
console.log(j);
},j*1000)
}
需要注意的是,for循环头部的let声明还会有一个特殊的行为,这个行为指出变量在循环过程分钟不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。