小谈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 您的支持是我最大的动力!