闭包是怎样产生的

664 阅读5分钟

首先了解什么是闭包 

MDN:一个函数和对其周围状态(词法环境)的引用,捆绑在一起,这样的组合就是闭包 (closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;

简单来说:闭包(Closure)其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者说闭包是个内嵌函数。并且闭包是可以在浏览器后台通过断点调试或者打印查看的

  • 探究闭包是怎样产生的

根据定义,闭包是嵌套函数,我们看下面代码

    function fun() {
    let a = 1;
    let b = 2
    function fn() {}
    console.dir(fn)
    }
    fun()

我们用 console.dir(fn) 输出看看

可以看到函数fn不是闭包

那我们接着修改一下代码

    function fun() {
    let a = 1;
    let b = 2
    function fn() {
    a++
    }
    console.dir(fn)
    }
    fun()

因为存在作用域链,所以内部的函数是可以访问外层函数的变量a。这个时候我们打印发现

在函数fn的作用域上出现了Closure对象,这就说明函数fn是一个闭包函数。

观察代码发现:函数fn所处的作用域指向外层函数fun 和全局Global,并且保存外层函数的局部变量a的值。通过前后对比发现我们第二次代码中内部函数引用了外层函数的变量,才产生了闭包由此可见,闭包产生的本质就是:当前环境中存在指向父级作用域的引用。在fn中Closure对象没有b的值,因为并没有对其引用

所以我们判断形成闭包的两个条件是1.要有函数的嵌套2.内部函数必须引用外部函数的变量。

  • 闭包是什么时候产生的呢?

还是上面的代码,我们可以在浏览器后台通过断点来看

为什么刚刚执行let a=1的时候fn就形成了闭包呢,在js运行时会执行预编译,会将函数声明会提前到当前块级的最顶端,所以这是函数fn相当于在a变量声明之前。

我们也可以换个写法

改成函数表达式,这个时候函数fn就是undefined。因为使用函数表达式的时候,函数声明部分不会提升

继续往下执行

观察发现只要执行完内部函数定义(不需要调用),就会产生闭包

此时当我们函数fun执行完毕,闭包也就销毁了。

闭包的作用

在之前的代码中发现,在fn的作用域中存在变量a,如果我们试着将内部函数作为返回值,是否还可以得到这个a呢?往下看

我们把函数fun执行后的返回值保存在变量res中,res就是函数fun返回的内部函数。当fun()执行后,按理来说函数内的局部变量会被销毁,可是在res中依然保存了a的值。进一步的说,通常情况下,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,如果创建了一个闭包的话,这个函数的作用域就会一直保存到闭包不存在为止。

当我们继续执行res(),此时a的值变为了2,继续执行res(),a的值变为了3。也进一步证明,在函数的外部 我们也访问到了函数fun的局部变量。这就是因为闭包导致的。

因此可以得出闭包的第一个作用:延长了函数的作用域范围,延长了局部变量的生命周期

上面代码执行完后,会发现变量a的值会一直存在res中。那闭包是什么时候被销毁的呢,观察后台输出 Closure 是一个对象,让对象销毁的方法可以给他设置为null。我们测试一下:

   res=null;  最后执行完这句代码 闭包就不存在了

接下来还有一个有趣的

    function test() {
    let a = 2;
    function inner() {
     console.log(a)
    }
    console.dir(inner)
    function inner2() {
    }
    console.dir(inner2)
    }
    test()

根据前面的了解,很容易判断出函数inner满足所说的两个条件,是一个闭包

通过打印我们发现函数inner2并没用引用外部函数的变量 可也是一个闭包。

我们发现在这种内部同时嵌套函数时,只要有一个闭包函数,它的同级函数也会有闭包。

可以理解为,闭包具有“传染性

其实不止这样,测试发现只要是嵌套在内部的函数,有一个是闭包函数,产生的闭包现象会一直“传染”给当前内部所有的其他函数

  • 使用闭包解决经典问题

    for (var  i = 0; i < 3; i++) {
    function w() {
     console.log(i)
    }
    setTimeout(w, 1000) //3 3 3
    }
    

我们都知道这个最后输出是3个3,因为setTimeout是一个异步函数,会等for循环执行完毕后才执行,然后因为var 定义的i是全局变量,所以最后的i为3。

这里我们只先讲这个方法 那就是可以使用立即执行函数。

立即执行函数只有一个作用:创建一个独立的作用域。

    for (var  i = 0; i < 3; i++) {
    (function(j){
     function w() {
      console.log(j)
     }
     setTimeout(w, 1000) //0 1 2
    })(i)
   }

当每次循环的时候都会给立即执行函数传入一个实参i,然后立即函数执行的时候都会创建一个新的作用域,所以每次setTimeout访问到的i都是不同的。

那这个和闭包有什么关系呢?我们观察一下:因为在外层的 立即执行函数function 里面还包含着 内部函数w,而里面的 函数就访问了外层 function 的 j的值,由此就形成了一个闭包。

当然闭包的用途还有很多,咋们以后说