Blogger Information
Blog 13
fans 0
comment 0
visits 12524
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
js引擎的运行机制
so的博客
Original
676 people have browsed it

javascript从定义到执行,js引擎在实现层做了很多初始化工作,因此,在学习js引擎之前,需要引入几个相关的概念:执行环境栈,全局对象,执行环境,变量对象,活动对象,作用域和作用域链等,这些概念是js引擎工作的核心组件。

 

var x = 1;
       function A(y){
           var x = 2;
           function B(z){
               console.log(x+y+z);
           }
           return B;
       }
       var c = A(1);
       c(1); //3

 下面从全局初始化、执行函数A、执行函数B三个阶段来分析js引擎的工作机制:

全局初始化

JS引擎在进入一段可执行的代码时,需要完成以下三个初始化工作:

首先,创建一个全局对象(Global Object) , 这个对象全局只存在一份,它的属性在任何地方都可以访问,它的存在伴随着应用程序的整个生命周期。全局对象在创建时,将Math,String,Date,document 等常用的JS对象作为其属性。由于这个全局对象不能通过名字直接访问,因此还有另外一个属性window,并将window指向了自身,这样就可以通过window访问这个全局对象了。用伪代码模拟全局对象的大体结构如下:

 

//创建一个全局对象
var globalObject = {
   Math:{},
   String:{},
   Date:{},
   document:{}, //DOM操作
   ...
   window:this //让window属性指向了自身
}

然后,JS引擎需要构建一个执行环境栈( Execution Context Stack) ,与此同时,也要创建一个全局执行环境(Execution Context)EC ,并将这个全局执行环境EC压入执行环境栈中。执行环境栈的作用是为了保证程序能够按照正确的顺序被执行。在javascript中,每个函数都有自己的执行环境,当执行一个函数时,该函数的执行环境就会被推入执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。我们用伪代码来模拟执行环境栈和EC的关系:

 

var ECStack = []; //定义一个执行环境栈,类似于数组

var EC = {};   //创建一个执行空间,
//ECMA-262规范并没有对EC的数据结构做明确的定义,你可以理解为在内存中分配的一块空间

ECStack.push(EC); //进入函数,压入执行环境
ECStack.pop(EC);  //函数返回后,删除执行环境

最后,JS引擎还要创建一个与EC关联的全局变量对象(Varibale Object) VO,  并把VO指向全局对象,VO中不仅包含了全局对象的原有属性,还包括在全局定义的变量x 和函数 A,与此同时,在定义函数A的时候,还为 A 添加了一个内部属性scope,并将scope指向了VO。每个函数在定义的时候,都会创建一个与之关联的scope属性,scope总是指向定义函数时所在的环境。此时的ECStack结构如下:

 

ECStack = [   //执行环境栈
   EC(G) = {   //全局执行环境
       VO(G):{ //定义全局变量对象
           ... //包含全局对象原有的属性
           x = 1; //定义变量x
           A = function(){...}; //定义函数A
           A[[scope]] = this; //定义A的scope,并赋值为VO本身
       }
   }
];

执行函数A

当执行进入A(1) 时,JS引擎需要完成以下工作:

首先,JS引擎会创建函数A的执行环境EC,然后EC推入执行环境栈的顶部并获取执行权。此时执行环境栈中有两个执行环境,分别是全局执行环境和函数A执行环境,A的执行环境在栈顶,全局执行环境在栈的底部。然后,创建函数A的作用域链(Scope Chain) ,在javascript中,每个执行环境都有自己的作用域链,用于标识符解析,当执行环境被创建时,它的作用域链就初始化为当前运行函数的scope所包含的对象。  

接着,JS引擎会创建一个当前函数的活动对象(Activation Object) AO,这里的活动对象扮演着变量对象的角色,只是在函数中的叫法不同而已(你可以认为变量对象是一个总的概念,而活动对象是它的一个分支), AO中包含了函数的形参、arguments对象、this对象、以及局部变量和内部函数的定义,然后AO会被推入作用域链的顶端。需要注意的是,在定义函数B的时候,JS引擎同样也会为B添加了一个scope属性,并将scope指向了定义函数B时所在的环境,定义函数B的环境就是A的活动对象AO, 而AO位于链表的前端,由于链表具有首尾相连的特点,因此函数B的scope指向了A的整个作用域链。 我们再看看此时的ECStack结构:

 

ECStack = [   //执行环境栈
   EC(A) = {   //A的执行环境
       [scope]:VO(G), //VO是全局变量对象
       AO(A) : { //创建函数A的活动对象
           y:1,
           x:2,  //定义局部变量x
           B:function(){...}, //定义函数B
           B[[scope]] = this; //this指代AO本身,而AO位于scopeChain的顶端,因此B[[scope]]指向整个作用域链
           arguments:[],//平时我们在函数中访问的arguments就是AO中的arguments
           this:window  //函数中的this指向调用者window对象
       },
       scopeChain:<AO(A),A[[scope]]>  //链表初始化为A[[scope]],然后再把AO加入该作用域链的顶端,此时A的作用域链:AO(A)->VO(G)
   },
   EC(G) = {   //全局执行环境
       VO(G):{ //创建全局变量对象
           ... //包含全局对象原有的属性
           x = 1; //定义变量x
           A = function(){...}; //定义函数A
           A[[scope]] = this; //定义A的scope,A[[scope]] == VO(G)
       }
   }
];


执行函数B

函数A被执行以后,返回了B的引用,并赋值给了变量C,执行 C(1) 就相当于执行B(1),JS引擎需要完成以下工作:

首先,还和上面一样,创建函数B的执行环境EC,然后EC推入执行环境栈的顶部并获取执行权。 此时执行环境栈中有两个执行环境,分别是全局执行环境和函数B的执行环境,B的执行环境在栈顶,全局执行环境在栈的底部。(注意:当函数A返回后,A的执行环境就会从栈中被删除,只留下全局执行环境)然后,创建函数B的作用域链,并初始化为函数B的scope所包含的对象,即包含了A的作用域链。最后,创建函数B的活动对象AO,并将B的形参z, arguments对象 和 this对象作为AO的属性。此时ECStack将会变成这样:

 

ECStack = [   //执行环境栈
   EC(B) = {   //创建B的执行环境,并处于作用域链的顶端
       [scope]:AO(A), //指向函数A的作用域链,AO(A)->VO(G)
       var AO(B) = { //创建函数B的活动对象
           z:1,
           arguments:[],
           this:window
       }
       scopeChain:<AO(B),B[[scope]]>  //链表初始化为B[[scope]],再将AO(B)加入链表表头,此时B的作用域链:AO(B)->AO(A)-VO(G)
   },
   EC(A), //A的执行环境已经从栈顶被删除,
   EC(G) = {   //全局执行环境
       VO:{ //定义全局变量对象
           ... //包含全局对象原有的属性
           x = 1; //定义变量x
           A = function(){...}; //定义函数A
           A[[scope]] = this; //定义A的scope,A[[scope]] == VO(G)
       }
   }
];

当函数B执行“x+y+z”时,需要对x、y、z 三个标识符进行一一解析,解析过程遵守变量查找规则:先查找自己的活动对象中是否存在该属性,如果存在,则停止查找并返回;如果不存在,继续沿着其作用域链从顶端依次查找,直到找到为止,如果整个作用域链上都未找到该变量,则返回“undefined”。从上面的分析可以看出函数B的作用域链是这样的:

 

AO(B)->AO(A)->VO(G)

因此,变量x会在AO(A)中被找到,而不会查找VO(G)中的x,变量y也会在AO(A)中被找到,变量z 在自身的AO(B)中就找到了。所以执行结果:2+1+1=4.







Statement of this Website
The copyright of this blog article belongs to the blogger. Please specify the address when reprinting! If there is any infringement or violation of the law, please contact admin@php.cn Report processing!
All comments Speak rationally on civilized internet, please comply with News Comment Service Agreement
0 comments
Author's latest blog post