前端基础:闭包和作用域

69 阅读9分钟

js代码执行过程

 js代码的执行过程:
     a.语法分析:
         分析js脚本代码块的语法是否正确.如果不正确则抛出错误,如果正确则进入到预编译阶段
     b.预编译阶段
     c.执行阶段:
         1.创建全局的执行上下文并且压入当前栈
         2.每当遇到函数调用,会创建当前函数的执行上下文,并压入执行栈的顶部
         3.引擎会执行那些执行上下文位于栈顶的函数.当函数执行结束时,执行上下文从栈中弹出.然后到达下一个执行上下文
                                    

作用域

 作用域:
     JS中为静态作用域.
     函数的作用域在函数定义的时候就决定了.
     程序源代码中定义变量的区域,作用域规定了怎么去查找变量,以及确定当前执行代码对变量的访问权限.
     例1:
         var value = 1;
         function foo(){
             console.log(value);
             }
			
         function bar(){
             var value = 2;
             foo();
             }
             
         bar();//1
			
         //因为作用域在定义的时候就决定了
         //执行bar(),最后执行foo()
         //foo函数首先在foo中寻找value,然后在外部寻找value.最后打印1.2:
        var scope = 'global scope';
        function checkscope(){
            var scope = 'local scope';
            function f(){
                return scope;
            }
            return f();
        }
			
        checkscope();//"local scope"3:
        var scope = 'global scope';
        function checkscope(){
            var scope = 'local scope';
            function f(){
                return scope;
                }
            return f;
            }
			
        checkscope()();//'local scope'
                            
        //例2和例3中作用域在定义的时候就形成了
        //例2在f函数中返回scope时,会到上一层函数中去查找,最后返回"local scope"
        //例3在外部调用f()时,此时的作用域依旧像例2一样,所以返回scope时,会到上一层函数中去查找,最后"local scope"
                    

执行上下文

执行上下文栈(ECStack):
    js可执行代码:全局代码、函数代码、eval代码
    如何管理创建的执行上下文:通过执行上下文栈
    在javascript开始时,执行上下文栈就压入一个全局执行上下文
    当执行一个函数时.就会进行准备工作(执行上下文)
    只有整个应用程序结束时,执行上下文栈才会被清空

例1:执行上下文栈是如何处理执行上下文的
    function fun3(){
        console.log('func3');
        }

    function fun2(){
        fun3();
        }
    
    function fun1(){
        fun2();
        }
    
    fun1()://func3
                            
                            
    对应的执行上下文栈:
            //fun1()
            ECStack.push(<fun1>functionCOntext);
            
            //fun1()中调用fun2()
            ECStack.push(<fun2>functionContext);
			
            //fun2()中调用fun3()
            ECStack.push(<fun3>functionContext);
			
            //func3执行完毕
            ECStack.pop();
			
            //func2执行完毕
            ECStack.pop();
			
            //func1执行完毕
            ECStack.pop();
                            
                            
         例2:
             var scope = 'global scope';
             function checkscope(){
                 var scope = 'loacl scope';
                 function f(){
                     return scope;
                     }
                     return f();
             }
             checkscope();
             
             //对应的执行上下文栈
             ECStack.push(<checkscope>functionContext);
             ECStack.push(<f>functionContext);
			
             //此时先去寻找,但未找到
             ECStack.pop();
             //然后到上一个执行上下文中去寻找,找到scope
             ECStack.pop();
                   
             例3:
                 var scope = 'global scope';
                 function checkscope(){
                     var scope = 'local scope';
                     function f(){
                         return scope;
                         }
                         return f;
                     }
			
                 checkscope()();
			
                 //对应执行上下文栈
                 ECStack.push(<checkscope>functionContext);
                 ECStack.pop();
                 ECStack.push(<f>functionCOntext);
                 //此时去寻找scope
                 ECStack.pop();
                           

变量对象

  变量对象(GOVOAO):
      a.变量对象(Variable object):VO
      b.作用域链(Scope chain)
      c.this
		
  变量对象VO是和执行上下文相关的特殊对象,用来存储上下文的函数声明,函数形参和变量.
  !!不同执行上下文下的变量对象,稍有不同
		
  全局上下文下的全局变量GO
      a.全局对象GO,是一个对象
          console.log(this instanceof object);//true
      b.全局对象是预定义的对象,作为JavaScript的全局函数和全局属性的占位符
      c.通过使用全局对象,可以访问所有其他预定义的对象、函数和属性
          //效果一样
          console.log(Math.random());
          console.log(this.Math.random());
			
      d.全局对象是作用域链的头,总是在执行上下栈的栈底.只有在程序销毁时,才会出栈
      e.访问GO
          方法1:
              console.log(this);
		
          方法2:
              console.log(window)
                                    
          执行上下文的生命周期:
              包含三个阶段:创建阶段->执行阶段->回收阶段
                                          a.进入执行上下文
                                          b.执行代码
                                          c.回收阶段(GC)
                                            
          进行创建执行上下文(此时还未执行代码):
                  创建上下文阶段:
                      1.初始化作用域链
                      2.创建变量对象:
                          a.创建arguments对象,检查上下文,初始化参数名称和值并创建引用的赋值
                          b.扫描上下文的函数声明
                              (1)为每个函数,在变量对象上创建一个属性且执行函数在内存的地址
                              (2)如果函数的名称已经存在,引用指针被重写
                          c.扫描上下文的变量声明
                              (1)创建变量属性,且将变量的初始化为undefined
                              (2)如果变量的名称已经存在,则不会进行任何操作
                              
                      3.求出上下文内部this的值
                          例1:如何理解函数声明过程中如果变量对象已经包含了相同名字的属性,则替换
                              function foo1(a){
                                  console.log(a);
                                  function a(){};
                                  }
						
                              foo1(20);//'function a(){}'2:如何理解变量声明过程中如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响
                              function foo2(a){
                                  console.log(a);
                                  var a = 10;
                                  }
					
                              foo2('20');//'20'3:
                              function foo2(){
                                  console.log(a);
                                  var a = 10;
                                  function a(){};
                                  }
						
                              foo2();//'function a(){}'4:函数声明比变量优先级要高,并且定义过程不会被变量覆盖,除非时赋值
                              function foo3(a){
                                  var a = 10;
                                  function a(){};
                                  console.log(a);
                                  }
                               
                               foo3(20);//'10'5:
                              function foo3(a){
                                  var a;
                                  function a(){};
                                  console.log(a);
                                  }
                              
                              foo3(20);//'function a(){}'
                                                    
                              当进入的是函数上下文,变量对象会包括:
                                    a.函数的所有形参
                                        1.由名称和对应值组成的一个变量对象的属性被创建
                                        2.没有实参,属性值设置为undefined
                                        
                                    b.函数声明
                                        1.由名称和对应值组成一个变量对象的属性被创建(函数对象)
                                        2.如果变量对象已经存在相同名称的属性,则完全替换这个属性
                                    c.变量声明
                                        1.由名称和对应值(undefined)组成一个变量对象的属性被创建
                                        2.如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
        例1
           function foo(a){
                var b = 2;
                function c(){
		var d = function (){};
		b = 3;
                }
	}
            
        //进入执行上下文之前
            AO={
                arguments:{
                    0:1,
                    length:1
                    }
                }
        
        //进入执行上下文时的AO
            AO = {
                arguments:{
                    0:1,
                    length:1
                    },
                a:1,//若没有传实参,则a:undefined
                b:undefined,
                c:reference to function c(){},
                d:undefined,
	}
            
            //代码执行时的AO
            在代码执行阶段,会修改变量对象的属性值
            AO = {
                arguments:{
                    0:1,
                    length:1
                    },
                    a:1,
                    b:3,
                    c:reference to function c(){},
                    d:reference ro FucntionExpression 'd'
            }
                                    
                

作用域链

作用域链:	
    在作用域中去寻找变量时,会先从当前上下文的变量对象中查找,如果没有找到.
    就会从父即执行上下文(执行栈中的下一层)的变量对象中查找,一直找到全局上下文的变量对象.
    作用域链:
        这样由多个执行上下文的变量对象构成的链表
        
    创建过程:
        函数创建时,函数内部属性[[scope]],就会保持所有父变量对象在其中,之后执行上下文时,会将活动对象添加到作用域的最前面
        
    静态作用域的形成:
        //函数内部有内部属性[[scope]],存放上一级的变量对象
		
例1:
        function foo(){
            function bar(){
                ...   
                }
        }
			
        //创建未执行时,各自的[[scope]].且scope中存储上级的执行上下文对象,越高级放在数组最后面
        foo.[[scope]] = [
            globalContext.VO;
            ]
        
        bar.[[scope]] = [
            fooContext.AO,
            globalContext.VO,
            ]
            
        //函数调用时,进入函数上下文,创建AO/VO,就会将活动对象添加到作用域的前面
        Scope = [AO].concat([[Scope]]);
        
        //concat的使用说明
            var a = [1,2,3];
            var b = [4,5,6];
            console.log(a.concat(b));//[1,2,3,4,5,6]
            
执行上下文栈和变量对象来解释作用域链的创建过程
     例1:
         var scope = 'global scope';
         function checkscope(){
                 var scope2 = 'local scope';
                 return scope2;
                 }
                 
         checkscope();
         1.checkscope函数创建,保存作用域到内部属性[[scope]]
             checkscope.[[scope]] = [
                 globalContext.VO
                 ]
                 
         2.执行checkscope函数,创建checkscope函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
             ECStack = [
                 checkscopeContext,
                 globalContex,
                 ]
         3.checkscope函数不立即执行:
             开始做准备工作:
                 第一步:复制函数[[scope]]属性创建作用域链Scope     
                     checkscopeContext = {
				Scope:checkscope.[[scope]],
		 }
                 
                 第二步:用argumnets创建活动对,随后初始化活动对象,加入形参、函数声明、变量声明
                     checkscopeContext = {
                         AO:{
                             arguments:{
                                 length:0,
                                 },
                         scope2:undefined
                         },
                         Scope:checkscope.[[scope]],
                     }
                     
                 第三步:将活动对象AO压入checkscope作用域顶端
                     checkscopeContext = {
                         AO:{
                             arguments:{
                                 length:0,
                                 },
                             scope2:undefined
                             },
                         Scope:[AO,[[Scope]]]
							}
         4.准备工作做完,开始执行函数,修改AO的属性值
                 checkscopeContext = {
                     AO:{
                         arguments:{
                            length:0
                        },
                        scope2:"local scope"
                    },
                    Scope:[AO,[[Scope]]]
                 }
         
         5.查找到scope2的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹
             ECStack = [
                 globalContext
                 ]          
                       

闭包

a.理论角度:
    所有的函数都是闭包.因为在创建的时候就被上层上下文的数据保存起来了.
    因此在函数中访问全部变量,就相当于是在访问自由变量,这个时候使用最外层的作用域.

b.实践角度:
    1.即使创建它的上下文已经销毁,它仍然存在(内部函数从父函数中返回)
    2.在代码中引用自由变量
        例子:
            var scope = 'global scope';
            function checkscope(){
                var scope = "local scope";
                function f(){
                    return scope;
                }
                return f;
             }
            
            var foo = checkscope();
            foo();
            
            分析:
                1.进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
                2.全局执行上下文初始化
                3.执行checkscope函数,创建checkscope函数执行上下文,checkscope执行上下文被压入执行上下文栈
                4.checkscope执行上下文初始化,创建变量对象、作用域链、this等
                5.checkscope函数执行完毕,checkscope执行上下文从执行上下文栈中弹出
                6.执行f函数,创建f函数执行上下文,f执行上下文被压入执行上下文栈
                7.f执行上下文初始化,创建变量对象、作用域链、this等
                8.f函数执行完毕,f函数上下文从执行上下文栈弹出
                
                //f函数是如何在checkscope函数上下文被销毁后,取到checkscope作用域下的scope值
                //f执行上下文维护了一个作用域链:
                    fContext = {
                        Scope: [AO,checkscopeContext.AO,globalContext.VO]
                        }
                //所有f函数依然可以读取到checkscopeContext.AO的值
                //所以当f函数依旧引用checkscopeContext.AO中的值时,即使checkscopeContext的执行上下文被销毁了,但依旧会让checkscopeContext.AO存活在内存当中(正常情况下,执行完后会被垃圾回收).
                
        闭包面试题:
            例1:
                var data = [];
                for (var i = 0; i < 3; i++) {
                    data[i] = function () {
                        console.log(i);
                        };
                    }
                        
                data[0]();
                data[1]();
                data[2]();
            	
                //for循环结束后的GO
                //globalContext:{
                    VO:{
                        data:[...],
                        i:3,
                        }
            	}
            	
                所以:根据作用域链去寻找globalCOntext中的i
                data[0]();
                data[1]();
                daat[2]();
            	
                //for循环结束后的GO
                //globalContext:{
                    VO:{
                        data:[...],
                        i:3,
                        }
            	}
                
                所以:根据作用域链去寻找globalContext中的i
                data[0]Context = {
                    Scope:[AO,globalContext.VO]
            	}
                    //data[0]Context的AO并没有i值,所以会从globalContext.VO中查找,i为3,所以打印的结果就是3
                    data[0]();//3
                    data[1]();//3
                    data[2]();//3
            	
            例2:
                var data = [];
                for(var i=0;i<3;i++){
                    data[i] = (function(i){
                        return function (){
                            console.log(i);
                        }
                    })
                 }
                 
                //此时:
                     globalContext = {
                         VO:{
                             data:[...],
                             i:3
                         }
		 }
				
                //data[0]COntext = {
                    Scope:[AO,匿名函数Context.AO,globalContext.VO]
                    }
				
                //此时的匿名函数Context = {
                        AO:{
                            arguments:{
                            0:0,
                            length:1
                            },
                        i:0;为什么存在
                        }
                    }

                //根据作用域链查找时,匿名函数Context.AO中查找,因为i为0
                        data[0]();//0
                        data[1]();//1
                        data[2]();//2