这几天在看《javascript高级程序设计》,看到执行环境和做用域链的时候,就有些模糊了。书中仍是讲的不够具体。
经过上网查资料,特来总结,以备回顾和修正。javascript
要讲的依次为:前端
EC(执行环境或者执行上下文,Execution Context)java
ECS(执行环境栈Execution Context Stack)git
VO(变量对象,Variable Object)|AO(活动对象,Active Object)github
scope chain(做用域链)和[[scope]]属性数组
<!--more-->浏览器
每当控制器到达ECMAScript可执行代码的时候,控制器就进入了一个执行上下文(好高大上的概念啊)。函数
javascript中,EC分为三种:this
全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最早进入的就是这个环境。设计
函数级别的代码 – 当执行一个函数时,运行函数体中的代码。
Eval的代码 – 在Eval函数内运行的代码。
EC创建分为两个阶段:进入执行上下文和执行阶段。
1.进入上下文阶段:发生在函数调用时,可是在执行具体代码以前(好比,对函数参数进行具体化以前)
2.执行代码阶段:变量赋值,函数引用,执行其余代码。
咱们能够将EC看作是一个对象。
EC={ VO:{/* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */}, this:{}, Scope:{ /* VO以及全部父执行上下文中的VO */} }
一系列活动的执行上下文从逻辑上造成一个栈。栈底老是全局上下文,栈顶是当前(活动的)执行上下文。当在不一样的执行上下文间切换(退出的而进入新的执行上下文)的时候,栈会被修改(经过压栈或者退栈的形式)。
压栈:全局EC-->局部EC1-->局部EC2-->当前EC
出栈:全局EC<--局部EC1<--局部EC2<--当前EC
咱们能够用数组的形式来表示环境栈:
ECS=[局部EC,全局EC];
每次控制器进入一个函数(哪怕该函数被递归调用或者做为构造器),都会发生压栈的操做。过程相似javascript数组的push和pop操做。
当javascript代码文件被浏览器载入后,默认最早进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数建立一个新的执行上下文,而且将其压入到执行上下文堆栈的顶部。浏览器老是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,而后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行而且弹出堆栈,直到回到全局的上下文。
每个EC都对应一个变量对象VO,在该EC中定义的全部变量和函数都存放在其对应的VO中。
VO分为全局上下文VO(全局对象,Global object,咱们一般说的global对象)和函数上下文的AO。
VO: { // 上下文中的数据 (变量声明(var), 函数声明(FD), 函数形参(function arguments)) }
1.进入执行上下文时,VO的初始化过程具体以下:
函数的形参(当进入函数执行上下文时)
—— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined
函数声明(FunctionDeclaration, FD) —— 变量对象的一个属性,其属性名和值都是函数对象建立出来的;若是变量对象已经包含了相同名字的属性,则替换它的值
变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名即为变量名,其值为undefined;若是变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
注意:该过程是有前后顺序的。
2.执行代码阶段时,VO中的一些属性undefined值将会肯定。
在函数的执行上下文中,VO是不能直接访问的。它主要扮演被称做活跃对象(activation object)(简称:AO)的角色。
这句话怎么理解呢,就是当EC环境为函数时,咱们访问的是AO,而不是VO。
VO(functionContext) === AO;
AO是在进入函数的执行上下文时建立的,并为该对象初始化一个arguments属性,该属性的值为Arguments对象。
AO = { arguments: { callee:, length:, properties-indexes: //函数传参参数值 } };
FD的形式只能是以下这样:
function f(){ }
VO示例:
alert(x); // function var x = 10; alert(x); // 10 x = 20; function x() {}; alert(x); // 20
进入执行上下文时,
ECObject={ VO:{ x:<reference to FunctionDeclaration "x"> } };
执行代码时:
ECObject={ VO:{ x:20 //与函数x同名,替换掉,先是10,后变成20 } };
对于以上的过程,咱们详细解释下。
在进入上下文的时候,VO会被填充函数声明; 同一阶段,还有变量声明“x”,可是,正如此前提到的,变量声明是在函数声明和函数形参以后,而且,变量声明不会对已经存在的一样名字的函数声明和函数形参发生冲突。所以,在进入上下文的阶段,VO填充为以下形式:
VO = {}; VO['x'] = <引用了函数声明'x'> // 发现var x = 10; // 若是函数“x”还未定义 // 则 "x" 为undefined, 可是,在咱们的例子中 // 变量声明并不会影响同名的函数值 VO['x'] = <值不受影响,还是函数>
执行代码阶段,VO被修改以下:
VO['x'] = 10; VO['x'] = 20;
以下例子再次看到在进入上下文阶段,变量存储在VO中(所以,尽管else的代码块永远都不会执行到,而“b”却仍然在VO中)
if (true) { var a = 1; } else { var b = 2; } alert(a); // 1 alert(b); // undefined, but not "b is not defined"
AO示例:
function test(a, b) { var c = 10; function d() {} var e = function _e() {}; (function x() {}); } test(10); // call
当进入test(10)
的执行上下文时,它的AO为:
testEC={ AO:{ arguments:{ callee:test length:1, 0:10 }, a:10, c:undefined, d:<reference to FunctionDeclaration "d">, e:undefined } };
因而可知,在创建阶段,VO除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。函数表达式不会对VO形成影响,所以,(function x() {})
并不会存在于VO中。
当执行test(10)
时,它的AO为:
testEC={ AO:{ arguments:{ callee:test, length:1, 0:10 }, a:10, c:10, d:<reference to FunctionDeclaration "d">, e:<reference to FunctionDeclaration "e"> } };
可见,只有在这个阶段,变量属性才会被赋具体的值。
在执行上下文的做用域中查找变量的过程被称为标识符解析(indentifier resolution),这个过程的实现依赖于函数内部另外一个同执行上下文相关联的对象——做用域链。做用域链是一个有序链表,其包含着用以告诉JavaScript解析器一个标识符到底关联着哪个变量的对象。而每个执行上下文都有其本身的做用域链Scope。
一句话:做用域链Scope其实就是对执行上下文EC中的变量对象VO|AO有序访问的链表。能按顺序访问到VO|AO,就能访问到其中存放的变量和函数的定义。
Scope定义以下:
Scope = AO|VO + [[Scope]]
其中,AO始终在Scope的最前端,否则为啥叫活跃对象呢。即:
Scope = [AO].concat([[Scope]]);
这说明了,做用域链是在函数建立时就已经有了。
那么[[Scope]]是什么呢?
[[Scope]]是一个包含了全部上层变量对象的分层链,它属于当前函数上下文,并在函数建立的时候,保存在函数中。
[[Scope]]是在函数建立的时候保存起来的——静态的(不变的),只有一次而且一直都存在——直到函数销毁。 比方说,哪怕函数永远都不能被调用到,[[Scope]]属性也已经保存在函数对象上了。
var x=10; function f1(){ var y=20; function f2(){ return x+y; } }
以上示例中,f2的[[scope]]属性能够表示以下:
f2.[[scope]]=[ f2OuterContext.VO ]
而f2
的外部EC的全部上层变量对象包括了f1
的活跃对象f1Context.AO,再往外层的EC,就是global对象了。
因此,具体咱们能够表示以下:
f2.[[scope]]=[ f1Context.AO, globalContext.VO ]
对于EC执行环境是函数来讲,那么它的Scope表示为:
functionContext.Scope=functionContext.AO+function.[[scope]]
注意,以上代码的表示,也体现了[[scope]]和Scope的差别,Scope是EC的属性,而[[scope]]则是函数的静态属性。
(因为AO|VO在进入执行上下文和执行代码阶段不一样,因此,这里及之后Scope的表示,咱们都默认为是执行代码阶段的Scope,而对于静态属性[[scope]]而言,则是在函数声明时就建立了)
对于以上的代码EC,咱们能够给出其Scope的表示:
exampelEC={ Scope:[ f2Context.AO+f2.[[scope]], f1.context.AO+f1.[[scope]], globalContext.VO ] }
接下来,咱们给出以上其它值的表示:
globalContext.VO
globalContext.VO={ x:10, f1:<reference to FunctionDeclaration "f1"> }
f2Context.AO
f2Context.AO={ f1Context.AO={ arguments:{ callee:f1, length:0 }, y:20, f2:<reference to FunctionDeclaration "f2"> } }
f2.[[scope]]
f2Context.AO={ f1Context.AO:{ arguments:{ callee:f1, length:0 }, y:20, f2:<reference to FunctionDeclaration "f2"> }, globalContext.VO:{ x:10, f1:<reference to FunctionDeclaration "f1"> } }
f1Context.AO
f1Context.AO={ arguments:{ callee:f1, length:0 }, y:20, f2:<reference to FunctionDeclaration "f2"> }
f1.[[scope]](f1的全部上层EC的VO)
f1.[[scope]]={ globalContext.VO:{ x:undefined, f1:undefined } }
好,咱们知道,做用域链Scope呢,是用来有序访问VO|AO中的变量和函数,对于上面的示例,咱们给出访问的过程:
x,f1
- "x" -- f2Context.AO // not found -- f1Context.AO // not found -- globalContext.VO // found - 10
f1的访问过程相似。
y
- "y" -- f2Context.AO // not found -- f1Context.AO // found -20
咱们发现,在变量和函数的访问过程,并无涉及到[[scope]],那么[[scope]]存在的意义是什么呢?
这个仍是看下一篇文章吧。
EC分为两个阶段,进入执行上下文和执行代码。
ECStack管理EC的压栈和出栈。
每一个EC对应一个做用域链Scope,VO|AO(AO,VO只能有一个),this。
函数EC中的Scope在进入函数EC时建立,用来有序访问该EC对象AO中的变量和函数。
函数EC中的AO在进入函数EC时,肯定了Arguments对象的属性;在执行函数EC时,其它变量属性具体化。
函数的[[scope]]属性在函数建立时就已经肯定,并保持不变。