之前有讲过,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 您的支持是我最大的动力!