美文网首页Web 前端开发
函数作用域与闭包

函数作用域与闭包

作者: 黎贝卡beka | 来源:发表于2017-11-22 22:37 被阅读0次

    函数作用域

    要理解闭包,必须从理解函数被调用时都会发生什么入手。

    我们知道,每个javascript函数都是一个对象,其中有一些属性我们可以访问到,有一些不可以访问,这些属性仅供JavaScript引擎存取,是隐式属性。[[scope]]就是其中一个。
    [[scope]]就是我们所说的作用域,其中存储了执行期上下文的集合。由于这个集合呈链式链接,我们把这种链式链接叫做作用域链。

    当函数被定义(创建)时有一个自己所在环境的作用域(GO全局作用域 ,若是在函数内部,就是引用别人的作用域),当函数被执行时,会将自己的独一无二的AO(活动对象,是使用arguments和该函数内部的变量值初始化的活动对象)执行上下文放在前端,形成一个作用域链;当该函数执行完,自己的AO会被干掉,回到被定义时的状态。

    另外,变量的查找,就是找所在函数的作用域,首先从作用域的顶端开始查找,找不到的情况下,会查找外部函数的活动对象,依次向下查找,直到到达作为作用域链终点的全局执行环境。

    下面看几个查找变量例子,深入理解函数作用域及作用域链。

    function a(){
       function b(){
          var b=2223;  
       }
       var a=78;
    }
    a()
    b()
    console.log(b)
    

    输出结果: error: b is not defined
    当函数a执行完毕后,该函数内部的活动对象AO就会被销毁。所以函数外部是访问不到函数内部的变量的。


    function outer(){
       function inner(){
          var b=2223;   
          a=0
       }
       var a=78;
       inner() //①
       console.log(a) 
       console.log(b)
    }
    outer() 
    

    输出结果: 0 , error: b is not defined

    当函数inner在被定义的阶段,就会拥有(引用)函数outer的作用域(包括函数outer自己局部的活动对象AO和全局作用域);当函数inner()被执行的时候,会再创建一个自己的活动对象AO并被推入执行环境作用域链的前端。
    inner()函数在被执行的时候,由于变量a在outer()函数中已经存在并被inner()引用,所以inner()函数内部的变量a会修改掉外部函数变量a的值,并且可以不用声明。当inner()函数执行完毕后(执行到①处),inner()函数局部的AO会被销毁,下面就访问不到变量b了,而且这时候变量a的值将是被inner()函数修改过的值。


    function a(){
      function b(){
         var b=2223;   
         a=0
      }
      var a=78;
      b=1
      b()
      console.log(b)
    }
    a()
    

    输出结果: 1


    var x=10;
    function a(){
        console.log(x);
    }
    function b(){
        var x=20;
        a();
    }
    a();//10
    b();//还是10;
    

    总之:函数在被定义阶段,会引用着其所在环境的作用域;执行阶段,会创建一个自己独一无二的活动对象AO,并推入执行环境作用域链的前端;函数执行完毕之后,自己的执行上下文AO会被销毁,所以,这时候访问其内部的变量是访问不到的。但是,闭包的情况又有不同。

    简述什么是闭包

    闭包:有权访问另一个函数作用域中的变量的函数。

    从理论的角度上,所有的JavaScript函数都是闭包,因为函数在被定义阶段就会存储一个自己所在环境的作用域,可以访问这个作用域中的所有变量。可以说,闭包是 JS 函数作用域的副产品。理解js作用域,自然就明白了闭包,即使你不知道那是闭包。

    从技术实践的角度,以下函数才算闭包:

    1. 定义该函数的执行环境的作用域(执行上下文)即使被销毁 ,它的活动对象(AO)仍然会留在内存中,被该函数引用着。
    2. 引用了函数体外部的变量。

    创建闭包常见方式,就是在一个函数A内部创建另一个函数B,然后通过return这个函数B以便在外部使用,这个函数B就是一个闭包。

    举个例子:

    //也算闭包
    var a = 1;
    function foo() {
        console.log(a);
    }
    foo();
    
    //函数内部定义函数
    var scope = "global scope";
    function checkscope(){
        var scope = "local scope";
        function f(){
            return scope;
        }
        return f;
    }
    checkscope()()  //这里相当于:
    //var foo = checkscope();
    //foo();
    

    输出结果:local scope
    f()函数在被定义阶段就被保存到了外部,这个时候就相当于外部的函数可以访问另一个函数内部的变量,f()函数会形成一个闭包。

    按照函数作用域的概念,当checkscope()执行完毕后,其局部的活动对象AO会被销毁;但是由于checkscope()函数执行完毕后返回一个函数,根据函数在被定义阶段会引用该函数所在执行环境的执行上下文,被返回的函数f()即使被保存到了外部依然引用着checkscope()函数的执行期上下文,直到函数f()执行完毕,checkscope()函数的执行上下文才会被销毁。

    也就是说被嵌套的函数f()无论在什么地方执行,都会包含着外部函数(定义该函数)的活动对象。所以,即使f()被保存到外部,也可以访问到另一个函数checkscope()中定义的变量。

    无论通过何种手段将内部函数传递到所在的词法作用域以外, 它都会持有对原始定义作用域的引用, 无论在何处执行这个函数都会使用闭包。

    由此看来,闭包可能会导致一个问题:导致原有作用域链不释放,造成内存泄漏(内存空间越来越少)。可以通过手动将被引用的函数设为null,来解除对该函数的引用,以便释放内存。

    闭包的作用

    1. 实现公有变量。
    function a(){
       var num=100;
       function b(){
          num++;
          console.log(num)
       }
       return b;
    }
    
    var demo=a();
    demo();//101
    demo();//102
    
    function test(){
       var num=100;
       function a(){
          num++;
       }
       function b(){
          num--;
       }
       return [a,b]
    
    }
    
    var demo=test()
    demo[0]();//101
    demo[1]();//100
    //函数a和函数b引用的是同一个作用域。
    
    1. 实现私有变量。

    闭包通常用来创建内部变量,使得这些变量不能被外部随意修改,同时又可以通过指定的函数接口来操作。
    通过在立即执行函数中return 将方法保存到外部等待调用,内部的变量由于是私有的,外部访问不到,可防止污染全局变量,利于模块化开发。

    var foo = ( function() { 
       var secret = 'secret'; 
       // “闭包”内的函数可以访问 secret 变量,而secret变量对于外部却是隐藏的 
       return { 
          get_secret: function () { 
             // 通过定义的接口来访问 secret 
                return secret; 
          }, 
          new_secret: function ( new_secret ) { 
             // 通过定义的接口来修改 secret 
             secret = new_secret; 
          } 
       }; 
    } () ); 
    foo.get_secret (); // 得到 'secret' 
    foo.secret; // undefined,访问不能 
    foo.new_secret ('a new secret'); // 通过函数接口,我们访问并修改了secret 变量 
    foo.get_secret (); // 得到 'a new secret'
    
    var name='bcd';
    var init=(function (){
       var name='abc';
       function callName(){
          console.log(name);
       }
    
       //其他方法
    
       return function () {
          callName();
          //其他方法
       }
    }())
    
    init () //abc
    

    闭包经典题

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

    输出结果:打印十个10
    数组每个值都是一个函数,每个函数对createFunctions()形成一个闭包,此时i都是引用createFunctions()中同一个i变量。

    function test(){
       var arr=[];
       for(var i=0;i<10;i++){
          (function(j){
             arr[j]=function(){
                console.log(j);
             }
          }(i))
       }
       console.log(i)//10 ,i还是10
       return arr
    }
    var myArr=test();
    for(var i=0;i<10;i++){
       myArr[i]()
    }
    

    输出结果:从0到9
    这次依然把数组每个值赋为函数,不同的是循环十次立即执行函数,并将当前循环的i作为参数传进立即执行函数,由于参数是按值传递的,这样就把当前循环的i保存下来了。

    闭包中的this对象

    在闭包中使用this对象可能会导致一些问题,结果往往不是预想的输出结果。
    看个例子:

      var name = "The Window";
      var object = {
        name : "My Object",
        getNameFunc : function(){
          return function(){
            return this.name;
          };
        }
      };
      alert(object.getNameFunc()());
    

    输出结果:The Window

    this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。匿名函数往往具有全局性,这里可以这样理解,没有任何对象调用这个匿名函数,虽然这个匿名函数拥有getNameFunc()的执行上下文。

    因为这个匿名函数拥有getNameFunc()的执行上下文,通过把外部函数getNameFunc()作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问到该对象了。

      var name = "The Window";
      var object = {
        name : "My Object",
        getNameFunc : function(){
          var that = this;
          return function(){
            return that.name;
          };
        }
      };
      alert(object.getNameFunc()());
    

    输出结果:My Object

    理解到这里,基本上就搞定了闭包了。

    学习资料

    JavaScript 里的闭包是什么?应用场景有哪些?

    相关文章

      网友评论

        本文标题:函数作用域与闭包

        本文链接:https://www.haomeiwen.com/subject/qiaivxtx.html