在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引擎的垃圾回收机制回收,而是成为全局变量。
所以我的的初步解释为:

  1. 由关键字var声明的变量存在变量提升的情况,循环结束以后变量i变为全局变量
  2. 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老师的前端补习班」关注我的微信公众号,那么就可以第一时间收到我的最新文章。


tonychen
1.2k 声望272 粉丝

下一篇 »
作用域与提升