关于JavaScript的作用域与闭包

128 阅读4分钟

关于执行上下文

首先,JS脚本在执行之前,会先经过一个预编译的过程。
如果发生了语法错误,便会报错,无法执行脚本。
除此以外,还会在脚本执行之前,做好一些准备工作,即创建执行环境,也就是"执行上下文"。

执行上下文类型
  • 全局执行上下文(Global Object 简称GO)

脚本在执行之前,会生成一个全局执行上下文,这是最基础也是最顶层的执行上下文,里面存放着window全局对象与声明的变量,并且将this指向window。
值得注意的是,一个程序只会存在一个GO。

// 为了直观感受,以对象做执行上下文的列子
        var a = 10;
        function b() { }

        GO = {
            window: window,
            a: undefined,
            b: function b() { },
            this: window
        }
  • 函数执行上下文

每当一个函数被声明时都会创建一个[[scope]]的属性,也就是我们俗称的作用域。
而函数被调用时,会创建一个函数执行上下文(Activation Object 简称AO), [[scope]]里面不仅保存着除本身函数调用时,所产生的函数执行上下文,还保存着其他执行上下文。
需要注意,函数的重复调用,会创建新的AO执行上下文。

        // 依旧以对象做执行上下文的列子
        var num1 = 5;
        var num2 = 6;
        function b() {
            var num1 = 10;
            console.log(num1);//输出10
            console.log(num2);//输出6
        }
        b();

        /*
        *    1-预编译阶段   
        *    GO = {
        *       num1 : undefined,
        *       num2 : undefined,
        *       b : function b() {}
        *    }
        *    GO产生  function b(){} 声明  创建了b.[[Scope]]作用域属性 且把GO存入进去  b.[[Scope]] = [GO]
        */

        /*
        *    2-脚本执行    b()开始调用 但函数体没执行阶段
        *    GO = {
        *       num1 : 5,
        *       num2 : 6,
        *       b : function b() {}
        *    }
        *    b函数的 执行上下文 创建 b-AO产生
        *    b-AO = {
        *       num1 : undefined,
        *       this : ?
        *    }
        *    默认的指向作者不了解,null or window?,但是后续谁调用this指向谁。下面例子省略this不写了
        */

        /*
        *    3- b()开始调用 函数体执行阶段
        *    GO = {
        *       num1 : 5,
        *       num2 : 6,
        *       b : function b() {}
        *    }
        *  
        *    b-AO = {
        *       num1 : 10,
        *    }
        */
总结

关于b.[[Scope]]的值,作者用了数组[]来解释,只是为了方便。其实并不是哈,用栈来理解更合适。
回到正题: console.log(num1);//输出10
其实就是根据b.[[Scope]]作用域链一个个执行上文开始寻找num1的变量(索引0开始,便利作用域链数组),b-AO执行上下文存在num1:10,所以直接输出num1的对应值10。
console.log(num2);//输出6 同样道理,先在作用域链数组[0]--> b-AO里面寻找,找不到变量num2. 继续寻找[1]--> GO,发现存在num2,直接输出对应值6。 注意:在b函数执行完以后,b函数的执行上下文AO就会销毁,作用域链由[AO,GO]变成[GO]。

  • Eval 函数执行上下文(这个作者不了解,就不谈)

根据上面所诉,谈谈闭包,废话不说,先上代码

       var a = 5;
       function fn1() {
            var a = 10;
            return function fn2(){
                return a;
            }
        }
        var value = fn1();
        console.log(a); // 输出5
        console.log(value()); //输出10

         /*
         * 1- 预编译阶段------------------
         *  GO = {
         *      a:undefined,
         *      fn1:function fn1(){},
         *      value:undefined
         *  }
         *  函数fn1被声明,创建了Scope属性,fn1的 作用域链 数组形成  fn1.[[Scope]] =  [GO]
         */ 


         /* 2- var value = fn1(); fn1()调用阶段,函数体未执行 
         *  GO = {
         *      a:5,
         *      fn1:function fn1(){},
         *      value:undefined
         *  }
         *  fn1-AO = {
         *      a:undefined,
         *      fn2:function fn2(){}
         *  }
         *    fn1被调用  fn1-AO执行上下文创建,fn1的 作用域链 数组改变   fn1.[[Scope]] =  [fn1-AO,GO]
         *    于此同时 函数fn2被声明,创建了Scope属性,fn2的 作用域链 数组形成  fn2.[[Scope]] =  [fn1-AO,GO]
         */
        
         
         /* 3- var value = fn1(); fn1()调用阶段,函数体执行完毕
         *  GO = {
         *      a:5,
         *      fn1:function fn1(){},
         *      value:function fn2(){}
         *  }
         * A的AO = {
         *      a:10,
         *      fn2:function fn2(){}
         *  }
         *  
         *   fn1函数执行完毕, fn1的作用域链 销毁     fn1.[[Scope]] 不存在了
         *   fn2作用域链 保持不变  fn2.[[Scope]] =  [fn1-AO,GO]
         *   fn1.[[Scope]]内的fn1-AO销毁, 并不能影响 fn2.[[Scope]]内的fn1-AO
         */ 
         
         

         /* 4- console.log(a); // 输出5    全局环境下输出 a 属性的值  去GO里面找 a   所以是5
         *     
         *    分析  console.log(value()); //输出10  
         *    value ==  function fn2(){}   所以  value() ==  fn2()
         *    fn2 被调用 创建了 它自己的执行上下文
         *    fn2-AO = {}   fn2的 作用域链 数组改变  fn2.[[Scope]] =  [fn2-AO,fn1-AO,GO]
         *    return a; 开始执行  现在[0](fn2-AO)寻找,没有, 再寻找[1](fn1-AO),发现变量a,输出该属性的值,也就是10
         */    
最后总结

fn1函数所创建的fn1-OA执行上下文,本应该在fn1()执行完毕后销毁。
但fn1函数内的 fn2的[[Scope]]属性,保存住了fn1-OA执行上下文,导致fn1-OA执行上下文销毁不了。 而fn1-OA执行上下文内又存有所声明的变量,以及属性值。 再者GO上声明了变量,去保存了fn2函数,相当于fn2的[[Scope]]的作用域链完整的保存下来。 于是通过 fn2-AO -> fn1-AO -> GO 的顺序,寻找a,并输出对应的值。