JavaScript 从定义到执行, JS 引擎在实现层做了很多初始化工作, 因此在学习 JS 引擎工作机制之前, 我们需要引入几个相关的概念: 执行环境栈全局对象执行环境变量对象活动对象作用域和作用域链等, 这些概念正是 JS 引擎工作的核心组件这篇文章的目的不是孤立的为你讲解每一个概念, 而是通过一个简单的 demo 来展开分析, 全局讲解 JS 引擎从定义到执行的每一个细节, 以及这些概念在其中所扮演的角色
- var x = 1; // 定义一个全局变量 x
- function A(y) {
- var x = 2; // 定义一个局部变量 x
- function B(z) { // 定义一个内部函数 B
- console.log(x + y + z);
- }
- return B; // 返回函数 B 的引用
- }
- var C = A(1); // 执行 A, 返回 B
- C(1); // 执行函数 B
这个 demo 是一个闭包, 执行结果是 4, 下面我们将分全局初始化执行函数 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 时, 需要对 xyz 三个标识符进行一一解析, 解析过程遵守变量查找规则: 先查找自己的活动对象中是否存在该属性, 如果存在, 则停止查找并返回; 如果不存在, 继续沿着其作用域链从顶端依次查找, 直到找到为止, 如果整个作用域链上都未找到该变量, 则返回 undefined 从上面的分析可以看出函数 B 的作用域链是这样的:
AO(B)->AO(A)->VO(G)
因此, 变量 x 会在 AO(A)中被找到, 而不会查找 VO(G)中的 x, 变量 y 也会在 AO(A)中被找到, 变量 z 在自身的 AO(B)中就找到了所以执行结果: 2+1+1=4.
简单的总结语
了解了 JS 引擎的工作机制之后, 我们不能只停留在理解概念的层面, 而要将其作为基础工具, 用以优化和改善我们在实际工作中的代码, 提高执行效率, 产生实际价值才是我们的真正目的就拿变量查找机制来说, 如果你的代码嵌套很深, 每引用一次全局变量, JS 引擎就要查找整个作用域链, 比如处于作用域链的最底端 window 和 document 对象就存在这个问题, 因此我们围绕这个问题可以做很多性能优化的工作, 当然还有其他方面的优化, 此处不再赘述, 本文仅当作抛砖引玉吧!
来源: http://www.codeceo.com/article/javascript-you-must-know.html