《你不知道的JavaScript》第一部分作用域和闭包第4篇。
在掌握作用域的前提下,才能真正理解和识别闭包。
闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo(){
var a = 10;
return function(){
console.log(a*2);
}
}
var fn = foo();
fn(); //20 => 闭包的效果
上述示例中,fn函数的词法作用域能够访问foo()函数的内部作用域。foo()
函数以一个函数对象作为返回值。在foo()
函数执行后,其返回值被赋值给了变量fn
并调用fn()
。实际上只是通过不同的标识符引用调用了这个被作为返回值的函数对象。
fn()
可以被正常执行,并且还是在被自己定义的词法作用域之外执行。
这就是闭包的神奇了。通常一个函数在执行完毕后其内部作用域就会被销毁,但由于内部作用域此时仍然被标识符fn所指向的函数对象所引用,所以foo()
函数的内部作用域不会被销毁,而这个引用就是闭包。
关于闭包,概念拎的差不多,再说也就那样,这玩意还是要能真悟到,不然还是会想不通的。
下面说个闭包的典型应用,for循环。
for(var i=1; i<=5; i++){
setTimeout(function timer(){
console.log(i)
}, i*1000)
}
//输出 => 6 6 6 6 6
上例结果是以每秒一次的频率输出6,而非设想的每秒一次的输出数字1~5。
出现这样非预期的情况,原因如下:
理解上面的原因,需要理解两个知识点:js中同步异步的执行顺序原理、作用域的工作原理。
找出了问题的原因,解决办法也就浮出水面了。如果能够让每个延迟函数time()都处于一个局部作用域中,并且该局部作用域中存在相应的变量i,让延迟函数timer()来访问该变量,不就行了么?创建局部作用域可以使用匿名函数自执行(IIFE)来做。
好,来搞个
for(var i=1; i<=5; i++){
(function(){
setTimeout(function timer(){
console.log(i);
}, i*1000)
})()
}
一打印,哦哟,咋还是以每秒一次的频率输出6哇,这不是已经把每个延迟函数单独放进一个局部作用域中了么?
再仔细一看,局部作用域里是空的,没有变量i,逼的timer()只能访问外层作用域中的变量i,所以此时需要将外层作用域中每次循环出来的变量i都传进每个局部作用域中去:
for(var i=1; i<=5; i++){
(function (j){
setTimeout(function timer(){
console.log(j)
}, j*1000)
})(i)
}
完美,此时就能顺利的达到预期设想了:每秒一次的输出数字1~5。
再回过头想想这个,正确执行的原理就是:将每个循环出来的timer()延迟函数放到一个单独的块作用域中去,并将循环出来的变量i值传入该单独块作用域中,如此,在块作用域中的函数可以随时访问所处作用域中的变量i。这其实就是闭包的实现。一个函数可以始终访问到所处作用域中的变量,而不管这个函数是否会在该作用域之外执行。
想到块作用域,ES6中的let关键字不是可以主动生成块作用域的么,把上例改一下,可以更简便的实现预期设想:
for(let i=1; i<=5; i++){
setTimeout(function timer(){
console.log(i)
}, i*1000)
}
//输出 => 1 (1s) 2 (1s) 3 (1s) 4 (1s) 5
完美!
块作用域 + 闭包,简直不要太如鱼得水。
闭包的作用强大,还可以用来写模块。
function foo(){
var name = "nitx",
skills = ['frontend', 'backend', 'db'];
function showName(){
console.log(name);
}
function showSkills(){
var skillStr = skills.join("、");
console.log(skillStr);
}
return {
showName: showName,
showSkills: showSkills
}
}
var fn = foo();
fn.showName();
fn.showSkills();
/*
打印 =>
nitx
frontend、backend、db
*/
函数foo()在调用后形成一个闭包,其返回值是一个对象字面量,其作用是模块暴露,将其赋值给了一个模块实例标识符fn,通过fn来调用模块API方法。
上面这个示例中,每当foo()调用一次,就会生成一个模块实例,如果设想只能生成一个模块实例,可以使用IIFE来实现:
var fn = (function foo(){
var name = "nitx",
skills = ['frontend', 'backend', 'db'];
function showName(){
console.log(name);
}
function showSkills(){
var skillStr = skills.join("、");
console.log(skillStr);
}
return {
showName: showName,
showSkills: showSkills
}
})()
fn.showName();
fn.showSkills();
结果也是一样的。
这就是闭包的模块应用,当然在ES6版本后,官方提供了正规的模块模式,import
、export
等,这个可以自行去看相关资料,总之是很好用的。
最后来总结下闭包吧。
当函数可以始终记住并访问其所在作用域,即使该函数在该作用域之外执行,这种情况就产生了闭包。
闭包的形成需要两要素:存在访问目标的局部作用域、函数始终保持对该作用域的引用。
能用闭包的形式实现的,也可以用面向对象写法实现,反之亦然。