闭包你理解了么?

258 阅读5分钟

都说包治百病,包治百病,今天我也带了一个名牌包包,不知道能不能治疗百病,猜猜是什么牌子,考验你对牌子的认识能力的时刻到了,LV、Prada、HERMES、CHANEL、Gucci、Versace、Coach反正我就知道这几个了太难了,但是通通都不是。今天给大家带来的是闭包。JavaScript中闭包这个点,相信很多的前端程序猿都听说过,不管是初级的还是高级的,而且是面试当中很容易被问到的一个,大家可能在对这个点的理解上,不太深刻。当然大佬们轻喷我泛指我一类的渣渣,毕竟人外有人天外有天,始终保持敬畏还是好的。那我们今天就开搞吧

  1. 作用域

    提到闭包之前不得不提一下作用域的问题,在执行代码前很多的变量已经定义好。作用域里面保存的信息,在你写代码的时候已经决定了,而且会一直保持这个作用域不变。

    var a = 1;
    function fn(){
        var b= a+1;
        var m = 999;
        function handle(c){
            var n= a+ b+c;
            console.log(n);
        }
        handle(6);
    }
    fn()
    // 输出 9 
    

    我们用数学的集合的思想看一下作用域 。

    1、查找标识符的过程会始终从当前作用域开始,然后逐级地向外层嵌套的作用域展开,直到找到标识符,或抵达最外层的作用域(也就是全局作用域)为止,如果找不到标识符,通常会导致错误发生。 2、每个执行环境都可以进入到外层作用域中查找标识符,但不能进入到内层作用域中查找标识符。

  2. 闭包的定义

    闭包是指有权访问另一个函数作用域中的变量的函数。从定义中我们可以得出两个结论

    • 可以在函数的外部访问到函数内部的局部变量。
    • 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。

    JavaScript高级程序设计(第3版)对作用域链的描述如下:

    当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

    所以这样我们就知道,能够访问一个函数A内部变量的除了这个函数A本身之外,其函数A所生成的包含环境中所在的函数B也可以访问。而知道了这一点我们就可以很容易猜到闭包的原理。那就是既然函数A内部所在的其它函数B可以访问到当前函数A的内部变量,那么如果我们将其内部所在的其它函数B作为返回值将其返回,并在函数A的外部用一个变量C来接收到这个返回值。那么这样,在函数A外部操作这个变量C时,实际上就是在操作函数A的返回值,也就是函数A所生成的包含环境中所在的内部函数B。而这个内部函数B是有权访问到函数A的内部变量的,所以在函数A外部的变量也就可以访问到函数A内部的变量。

  3. 闭包的理解

    来吧 举栗子吧 不举栗子都不能讲清楚我的理解了 哈哈哈

    function fn() {
        var a = 1;
        function childFn() {
            console.log(a);
        }
        childFn();
    }
    fn();
    //输出:1
    

    上边的函数 属于闭包么? 那就看看函数会不会一直保持对定义时所处作用域的引用。上边的 栗子肯定能保持啊,只是好像不是我们看见的标准的那样的闭包写法,那我们就来改一下吧

    function fn(){
      var a = 1;
      function childFn(){
        console.log(a)
      }
      return childFn;
    }
    var handle = fn();
    handle();
    

    在创建fn函数时,会创建一个预先包含全局对象的作用域链 ,这个作用域链保存在内部的[[Scope]]属性当中。当调用fn函数的时候,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构成执行环境的作用域链,在函数childFn函数中实际上将fn的活动对象添加到了自己的作用域中,所以childFn函数的的作用域链被初始化为包含着父函数活动对象和全局变量对象。也就导致了当fn函数执行完毕后,并不会销毁,因为在childFn函数的作用域对象仍引用着这个活动。

  4. 闭包和变量

    作用域链的这种配置机制引出了一个副作用 ,即闭包只能取得包含中任何变量的最后的一个值

    function createFunctions(){
      var result  = new Array();
      for(var i = 0 ;i<10;i++){
        result[i]= function(){
          return i;
        }
      }
      return result;
    }
    createFunctions()
    

    函数回返回一个数组,表面上好像是返回每一个函数的都应该返回自己的索引值,即位置0 的函数返回 0 ,1 的位置返回1 以此类推。但实际上每个函数都返回的10 ,因为每个函数作用域中都保存着createFunctions()函数的活动对象,所以他们都是同一个变量当createFunctions()函数返回后,变量i的值都是10

  5. 回调函数中闭包的使用

    我们试一试回调函数中,是否仍然可以持有对定义时作用域的引用

    function fn() {
        var n = 111;
        function childFn() {
            console.log(n);
        }
        return childFn;
    }
    
    function runFn(callBack) {
        callBack();
    }
    runFn(fn());
    //输出:111
    

    同步函数的回调支持定义时作用域的引用

    function fn() {
        var n = 111;
        function childFn() {
            console.log(n);
        }
        return childFn;
    }
    
    function runFn(callBack) {
        setTimeout(function() {
            callBack();
        }, 2000)
    }
    runFn(fn());
    //输出:111