关于闭包

134 阅读3分钟

闭包是什么?

  • 红宝石(p178)中描述为:闭包是指有权访问另一个函数作用域中的变量的函数
  • MDN中描述为闭包让你可以在一个内层函数中访问到其外层函数的作用域。 综上:我们可以得知,闭包主要的作用为让一个函数可以访问到另一个函数作用域中的私有变量

一道经典的题目:

    for(var i =0;i<5;i++){
      setTimeout(()=>{
        console.log(new Date,i);
      },1000)
    }

    console.log(new Date,i);

一般人们会给出两种答案:

  • 5 0 1 2 3 4
  • 5 5 5 5 5 5 答案是第二种情况,为什么呢?
    表面上看似乎每个函数都应该返回自己的索引值,但实际上,每个setTimeout函数作用域都保存着同一个变量i,此时每个函数都引用着变量i的同一个变量对象i=4,至于前面的0 1 2 3 4都因为被重新赋值销毁了。

那么如何达到我们想要的效果呢?
可以使用闭包来实现:

    for(var i =0;i<5;i++){
     (function(j){
      setTimeout(()=>{
        console.log(new Date,j);
      },1000)
     })(i)
    }

    console.log(new Date,i);

为什么这样就能实现呢?

因为我们定义了一个匿名函数将i的值传入给参数j,在每次调用匿名函数时,由于函数是按值传递的,所以就会将变量i的值复制给j,而匿名函数内又创建了一个setTimeout函数并访问j的闭包,这样一来每个setTimeout函数中都有一个变量j的副本就可以实现我们的目标了

那么如果再问你setTimeout中的输出是每隔1s输出一次还是同时输出呢?

2021-12-29T14:25:44.115Z 5
2021-12-29T14:25:45.126Z 0
2021-12-29T14:25:45.127Z 1
2021-12-29T14:25:45.129Z 2
2021-12-29T14:25:45.130Z 3
2021-12-29T14:25:45.132Z 4

这是执行结果,显而易见,除了最外层的宏任务的输出是在25:44输出的,setTimeout中的输出几乎是同一时间执行的,因为setTimeout是异步执行,每一次for循环的时候,setTimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里,等待执行。 只有主线上的任务执行完,才会执行任务队列里的任务。

闭包的缺点

因为闭包不会被垃圾回收机制所回收,所以如果闭包的作用域链创建了一个HTML元素那就意味着元素将无法被销毁

function Test(){
    var ele = document.getElementById('test');
    ele.onclick = function(){
        alert(ele.id)
    };
}

因为javaScript中的垃圾回收机制是根据引用计数来回收垃圾的,具体可以查看(js垃圾回收机制原理),由于Test中存在,所以ele的引用数最少也是1,js垃圾回收机制是引用数为0的时候进行垃圾回收,所以导致ele占用的内存永远不会被回收,造成内存泄漏

该如何避免呢?

function Test(){
    var ele = document.getElementById('test');
    var id = ele.id
    ele.onclick = function(){
        alert(id)
    };
    ele = null;
}

在这里我们将ele.id的一个副本存入定义的id,这样在闭包引用该变量时消除了循环引用不直接引用ele.id,但是ele.onclick还是会引用到整个活动对象,所以有必要在最后将ele变量设置为null,这样就解除了对DOM对象的引用。

闭包的应用
在日常开发中,有很多地方用到闭包

  • 防抖节流
  • new关键字的实现

结束

本篇文章根据学习JavaScript高级程序设计(第三版)总结,希望大家能指出错误。