小谈javascript作用域链

    之前有讲过,JavaScript对象拥有原型链,有了原型链,可以让我们很容易的处理继承关系,今天来理解一下另一个比较重要的知识,作用域链。

    作用域链的作用:

    js引擎在获取变量值时,是按照作用域链由顶到底的顺序查找同名变量,首先找到的那个就是目标变量(这点和原型链上寻值类似,但本质上是两种不同的东西,原型链是针对对象层面的,而作用域是针对函数运行层面的)。

    我们来举个列子说明:

    function A(y){   
       var x = 2;  //定义一个局部变量 x
       function B(z){ //定义一个内部函数 B
           alert(x);
           alert(y);
           alert(z);
       }   
       return B; //返回函数B的引用
    }
    var x = 1;  //定义一个全局变量 x
    var C = A(1); //执行A,返回B
    C(1); //执行函数B

    执行结果是 2 1 1; 简单说明一下程序执行,首先是定义了一个函数A,然后定义了一个全局变量x,之后将A的执行结果返回给C,之后执行C。

    其中A的执行结果就是将B返回,然后执行C,其实就是执行B,然后B函数中并没有变量x和变量y,可是为什么最后x,y是有值的呢?要想搞明白这些,必须从js引擎的工作机制谈起。

    解释器的运行过程:

    js是一种解释型语言,所以不存在编译过程,而这里面一个重要的角色是解释器,一段js代码是如何开始运行的?首先必须先通过解释器。而解释器会对js代码进行分析,加载,运行。

    首先,解释器会创建一个全局对象(Global Object),这个对象整个运行中只建立一次,并且任何属性,任何地方都可以访问它,而且会将js引擎中内置的对象和function都注册到这个对象中,同时会初始化一个属性变量叫window,并指向当前对象。

    其实这便是window对象的来源。window对象是js引擎初始化时生成的一个对象。

    然后,解释器会构造一个执行环境栈(Execution Context Stack),这个栈的作用是为了记录当前程序的运行环境,因为每个函数都会有它自己的运行环境,又因为函数是可以嵌套的,所以牵涉到运行环境的切换,解释器定义的这个执行环境栈,栈顶环境就是当前正在执行的代码的执行环境。当栈顶环境执行完毕,就会弹出,由新的栈顶环境接管执行。

    之后,解释器会创建一个执行环境对象,一般执行环境对象包含三个属性,scope,scopeChain,varibaleObject,scorp记录当前执行环境,其实就是作用域,scopeChain是一个链表,表头是当前作用域,之后是它的父级作用域,即生成当前作用域的作用域,其实你会发现这个链表的顺序就是栈的顺序。varibaleObject,对应当前要执行的代码对象,主要包含函数的参数,内置变量,作用域。

    以下是解释器运行的伪代码

    var ECStack = [];//执行环境栈
    //var EC={};        //执行环境对象
    //ECStack.push(EC);   //将执行环境对象压入执行环境栈,栈顶环境开始执行
    
    //创建一个全局的执行环境对象
    var EC_gloable = {
        scope:window,
        scopeChain:[vo_gloable,window],
        vo_gloable:{
            A :function(){...},
            x : 1,
            A["scope"]=vo_gloable,
            C:excute::A,
            &0:excute::C,    //excute::A代表执行函数A 
            //$0:代码匿名执行函数
            &0:excute::end    //调用结束
        }
    }
    ECStack.push(EC_gloable);    //放入栈顶的那一刻开始执行
    //执行规则按照vo_gloable的顺序依次执行,当遇到excute时
    //制造一个新的执行环境对象
    var EC_A = {
        scope:vo_gloable,
        scopeChain:[vo_A,vo_gloable,window],
        vo_A:{
           y:1,//此处是当执行时 函数传进来的参数 也就是y值
           x:2,
           B:function(){...},
           B["scope"]:vo_A,
           arguments:[y=1],//这个就是我们常用的arguments,
           this:window//this指向运行时的调用上下文对象,这里是window对象
           //注意是调用上下文对象,并不是作用域
           //一定要区别调用上下文和作用域的概念
           &0:excute::end    //调用结束
        }
    }
    ECStack.push(EC_A);    //将生成的EC_A压入栈顶,开始执行函数A
    ECStack.pop(EC_A);    //当A函数执行完毕时 解释器会调用出栈操作,
    //此时ECStack的栈顶回归到EC_gloable 继续执行下面的操作 遇到函数C
    //生成新的执行环境对象 
    
    var EC_B = {
        scope:vo_A,    //这里要注意,C函数调用的上下文是window,
        //但定义function B的时候会将 作用域代入,而那时的作用域是functionA中
        //也就是vo_A
        scopeChain:[vo_B,vo_A,vo_gloable,window],
        vo_B:{
            z:1,//传进来的参数
            &0:excute::alert(x),
            &0:excute::alert(y),
            &0:excute::alert(z),
            arguments:[z=1],
            this:window,
            &0:excute::end    //调用结束
        }
    }
    
    ECStack.push(EC_B);    //将生成的EC_B压入栈顶,开始执行函数B
    ECStack.pop(EC_B);    //运行B结束
    ECStack.pop(EC_gloable);    //整体运行结束 ECStack清空

    以上便是js 解释器运行代码的一般流程,可以很清晰的看出解释器是如何边解析代码,边运行代码的。之后 我们来分析一下最终的执行结果,也就是最终alert出来的三个数字,是如何得出的。

    首先看alert(z)这个比较简单 是最后调用C时传入的z值1;

    再看 alert(y),y这个变量在vo_B中没有定义,按照最开始说的,解释器会按照作用域链的顺序往上查找,然后发现

    scopeChain:[vo_B,vo_A,vo_gloable,window],

    下一个作用域是vo_A,去vo_A对象下查找,发现存在y:1,所以y的值就是1;

    同理看 alert(x),x在vo_B中没有定义,延作用域链向上找vo_A,发现x:2,所以x的值为2,当然细心的同学会发现,在往上找,vo_gloable中也有x值为1,但是由于已经在vo_A中找到了x,所以自动就放弃了查找。

    以上就是js的运行原理,还有配合作用域链确定变量方法的分析过程,希望对大家有帮助。比较关键的点是一定要理解:

    1、作用域和调用上下文不是同一个概念

    2、作用域链和原型链有点相似,但本质也是两种不同的东西

    3、文中的js解释器运行过程只是伪代码,真正的解释器运行要比上文中的复杂很多,文章只是便于大家理解。

    以上是我对JavaScript作用域的理解,欢迎各位同学与我pk切磋。


    转载请注明出处:http://gagalulu.wang/blog/detail/11 您的支持是我最大的动力!