这又是一个老生常谈的js基础问题了,一千个人眼里有一千个哈姆雷特,同样,一千个人眼里也可能有一千个对闭包的理解,每个人对它的理解都会带一些自己的特色,而这个特色有时候又不太好表达,这就造成了网上对它的介绍实在太多太多,今天我就说一下我对闭包的认识。
闭包是基于词法作用域书写代码时所产生的自然结果,其实可能在你的代码中早已经到处是闭包了,现在缺少的是根据自己的意愿来识别,拥抱和影响闭包的思维环境。
阮一峰老师在博客里说 闭包就是能够读取其他函数内部变量的函数,我觉得说的特别通俗易懂,但也可以稍微扩展一下,这是我在书中看到的,我觉得可能更恰当一些。(阮一峰老师的很多博客都强烈建议新手去看一看,像es6,没有那些晦涩的术语,知识点介绍的也比较清晰,适合大家学习)
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这样就产生了闭包
废话不多说,上 板 栗 ~
先从简单的看
function foo () {
var a = 2;
function bar () {
console.log(a)
}
return bar;
}
var baz = foo();
baz(); //2 -朋友,这就是闭包的效果
在foo执行后,通常foo()整个内部作用域都被销毁,因为引擎有垃圾回收机制,不再使用的内存空间会被释放,但拜bar()声明的位置所赐,它依然持有对foo内部作用域的引用,拥有涵盖foo内部作用域的闭包,使得该作用域能够一直存活不被释放,这就叫闭包。
接着上板栗
function foo () {
var a = 2;
function baz () {
console.log(a)
}
bar(baz);
}
function bar (fn) {
fn() //这也是闭包
}
无论何种手段将内部函数传递到所在词法作用域之外,他都会持有对原始定义的作用域的引用,无论在何处执行这个函数都会产生闭包。
function wait (message) {
setTimeout(function timer (){
console.log(message)
},1000)
}
wait('hello') //这还是闭包
timer具有涵盖wait内部作用域的闭包。
而且,只要使用了回调函数,基本上都有闭包的存在。
循环与闭包
这有一个for循环最常见的例子
for(var i = 1; i<=5; i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
}
相信大家都能看出来,这段代码最终结果是输出5个6,而不是表面上看到的1到5。
这是为什么呢?
首先,因为js是单线程的,setTimeout会把函数放入执行队列中等待被执行,但队列中在他们前面有for循环,所以函数会等for循环结束后才开始执行,而有因为这些函数用的都是一个作用域内的i,for循环结束时i=6,所以结果为5个6
那如何才能解决这个问题呢
第一种,用立即执行函数(IIFE)
它的原理就是每次循环通过IIFE来创建闭包,这样每次循环时都会记录下当时的内部作用域,每次循环内部作用域都有不同,i都在变,把i传入这个IIFE中,这样内部的函数所引用的i就各不相同了,问题就解决了
for(var i = 1; i<=5; i++){
(function (j){
setTimeout(function timer(){
console.log(j)
},j*1000)
})(i)
}
第二种,块级作用域
es6引入了let声明(具体细节之后再讲,或大家可以先去了解一下),它有个特点是可以统治块级作用域。
回想下我们刚才的方法,我们用IIFE在每次循环生成新的作用域,也可以理解为每次生成一个块作用域,既然这样,我们就可以利用let劫持块作用域,本质上也就是将块转为一个可以被关闭的作用域,每次在里面声明变量,相当于每次循环都生成不同的块作用域,每个块里的东西互不干扰
for(let i = 1; i<=5; i++){
setTimeout(function timer(){
console.log(i)
},i*1000)
} //就是这么简单的解决了
for循环头部的let还有个特殊的行为,变量在声明过程中不止被声明一次,每次迭代都会声明,随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
今天就先到这里吧~