在JavaScript中,闭包其实随处可见。但真要说起什么是闭包的话,很多人可能一时半会不知道怎么解释。这篇博文主要写一些闭包的特性已经相关的应用。
参考资料:阮一峰——JavaScript运行机制详解
定义
闭包是指在函数声明时的作用域以外的地方被调用的函数,在函数声明时的作用域以外的地方调用函数,需要通过将该函数作为返回值或者作为参数被传递
特性
光看定义的话可能会比较懵逼(╯‵□′)╯︵┻━┻ 下面来看一下闭包到底都有哪些特性:
- 函数在自己定义的词法作用域以外的地方执行
- 函数嵌套
- 访问所在的词法作用域
循环闭包
定义
在循环中包含了函数定义则称为循环闭包
实例1
讲了这么多,下面来看一段例子:
for(var i = 1; i < 6; i++){
setTimeout(function(){
console.log(i);
},50);
}
//输出结果为: 6 6 6 6 6
好吧,如果你是新手的话,看到这里你是震惊的,因为作为一个萌新,当初我对这串代码的想法是这样的:
setTimeout函数在for循环中执行六次,每隔50ms输出一个数字,则会分别输出1、2、3、4、5
后来我发现,用关键字var
声明的变量会存在变量提升的情况,循环结束以后,变量i不会被JavaScript引擎的垃圾回收机制回收,而是成为全局变量。
所以我的的初步解释为:
- 由关键字
var
声明的变量存在变量提升的情况,循环结束以后变量i
变为全局变量 - for循环结束的比50ms要早,所以setTimeout函数读取不到迭代的变量
i
,而是循环结束后的变量i
实例2
然而上面只是我一开始错误的认识,直到我看到下面的代码:
for(var i=1;i<6;i++){
setTimeout(function(){
console.log(i);
},0);
}
//输出结果为: 6 6 6 6 6
因为这里setTimeout的时间参数是0,如果照着上面的思路来的话,这里的输出应该是 1 2 3 4 5
JavaScript调用机制
在讲述解决方法之前我们先来了解一下JavaScript的调用机制
对JavaScript比较熟悉的人应该了解,JavaScript是一种单线程的语言。而我们在编写代码的时候,往往不止同步的代码,还有异步执行的代码,JavaScript这里把它分成两种模式,一种叫做同步任务,另一种则叫做异步任务。在执行代码的时候,同步任务被分配在主线程中执行,形成一个调用栈;而异步任务则交给浏览器的其他线程去执行。当异步任务执行完毕以后,则将对应的异步任务放入任务队列中。当调用栈中的任务都执行完毕以后,再读取任务队列,取消对应任务的等待状态,然后进入调用栈,开始执行。
解决方法
-
方法1:绑定作用域,将关键字
var
改为let
,具体代码为:for(let i=1;i<6;i++){ setTimeout(function(){ console.log(i) },50) }
-
方法2:运用IIFE(立即执行函数),具体代码为:
for(var i=1;i<6;i++){ (function(j){ setTimeout(function(){ console.log(j) },50) })(i); }
在迭代内部使用IIFE会为每个迭代生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代都含有一个具有正确值的变量供我们访问
总结
闭包在JavaScript里随处可见,我们在使用闭包的时候,需要谨慎的在循环内部添加闭包。个人觉得最好的解决方案就是使用关键字let
,可读性强而且另代码整洁,希望ES6能够全面普及。
扫描下方的二维码或搜索「tony老师的前端补习班」关注我的微信公众号,那么就可以第一时间收到我的最新文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。