JavaScript
执行环境、作用域链
EC
的组成
当
js
代码执行时,会进入不同的执行上下文,这些执行上下文会形成一个执行上下文栈(Execution context stack
,ECS
)
js
中,执行环境分为三种:
- 全局执行环境 - 一旦代码被载入,全局执行环境被创建,在任何地方都可以访问到全局执行环境中的内容
- 局部执行环境 - 当执行某一个函数时,局部执行环境被创建,当函数执行完毕时,该局部执行环境被销毁,其中的所有内容也被销毁
-
Eval
- 在Eval
函数内运行时会被创建
ECS
:环境栈 – 执行上下文栈
在执行代码时,一系列活动的执行上下文从逻辑上形成一个栈。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,函数的环境(执行上下文)就会被推入一个该环境栈中,在函数执行完毕后,栈将其环境弹出,把控制权交还给之前的执行环境,直至回到全局的上下文(当应用程序退出后,例如关闭网页或浏览器时才会销毁全局执行环境)环境栈的栈底永远都是全局上下文,栈顶时当前执行上下文。当在不同上下文环境间切换时,会通过退栈入栈的形式完成。
eg
:
压栈:全局EC -> 局部EC1 -> 局部EC2 -> 当前EC
出栈:当前EC -> 局部EC2 -> 局部EC1 -> 全局EC
变量对象(VO
)和活动对象(AO
)
- 每一个
EC
都对应一个变量对象VO
,在该执行上下文中定义的变量、函数以及函数形参arguments
都存放在其中。- 如果当前执行上下文为函数,则
AO
(活动对象)会被作为当前执行上下文的变量对象AO
=VO
+ 函数形参 +arguments
。AO
在最开始只包含一个变量,即arguments
对象
下面以EC
建立的两个阶段来对AO
和VO
进行说明
例1:
//1
alert(a);//function
a();//233
var a = 1;
function a () {
console.log(233)
}
alert(a);//1
- 进入执行上下文
ECObject = {
VO: {
a : <reference to FunctionDeclaration "a">
}
}
- 执行阶段
ECObject = {
VO: {
a : 1
}
}
例2:
//2
function fun(m, n) {
console.log(a);//function
a();//233
var a = 1;
function a() {
console.log(233)
}
console.log(a);//1
}
fun(1,2)
- 进入执行上下文
ECObject = {
VO: {
arguments: {
callee: fun,
length: 2,
0: 1,
1: 2
},
m: 1,
n: 2,
a : <reference to FunctionDeclaration "a">
}
}
- 执行阶段
ECObject = {
VO: {
arguments: {
callee: fun,
length: 2,
0: 1,
1: 2
},
m: 1,
n: 2,
a: 1
}
}
[[Scope]]
属性 与 Scope
(作用域链)
- 在创建函数时,会创建一个预先包含全局变量对象以及所有祖先变量对象的作用域链,并将其保存在
[[Scope]]
属性中。当函数被执行时,会将[[Scope]]
属性中的对象复制到Scope
作用域链上,并会创建执行环境的变量对象(活动对象 – 对于函数而言)且将其推入执行环境作用域链(Scope
)的最顶端。[[Scope]]
是一个包含了所有上层变量对象的分层链,它是当前函数上下文的静态属性,从创建后便不会改变。当当前函数上下文环境被创建时,它便被保存在其中了,直到当前函数上下文环境被销毁,否则它一直会存在其中。Scope
=AO|VO
+[[Scope]]
Scope
中存储着当前执行环境的作用域链。实际上,它是对执行上下文EC
中的变量对象VO|AO
有序访问的链表,能够按照顺序访问到VO|AO
,便能够访问到其中的变量和属性。- 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐渐开始向后搜做,直至找到标识符为止(如果找不到,通常会导致错误发生)。全局执行环境的变量对象始终都是作用域链中的最后一个变量对象。
[[Scope]]
、Scope
、VO
例子:
let x=10;
function f1(){
let y=20;
function f2(){
return x+y;
}
}
f2
的[[Scope]]
可以表示如下:
f2.[[Scope]] = [
f1Context.AO,
globalContext.VO
]
f2
所在执行上下文Scope
可以表示为:
f2.[[Scope]] = [
f2Context.AO,
f1Context.AO,
globalContext.VO
]
由上可知:Scope
是执行上下文的属性,[[Scope]]
是函数的静态属性
给出以上提到的对象的值:
globalContext.VO
globalContext.VO = {
x:10,
f1:<reference to FunctionDeclaration "f1">
}
f1Context.AO
f1Context.AO = {
argumnets: {
callee:f1,
length:0
},
y:20,
f2:<reference to FunctionDeclaration "f2">
}
-
f1.[[Scope]]
(f1的所有上层EC
的VO
)
f1.[[scope]] = {
globalContext.VO
}
f1.Scope
f1.Scope = {
f1Context.AO,
f1.[[scope]]
}
f2Context.VO
f2Context.AO = {
argumnets: {
callee:f2,
length:0
}
}
-
f2.[[Scope]]
(f2的所有上层EC
的VO
)
f2.[[scope]] = {
f1Context.VO,
globalContext.VO
}
f2.Scope
f2.Scope = {
f2Context.VO,
f2.[[scope]]
}
作用域链例子:
function compare(value1,value2){
if(value1<value2){
return -1;
}else if(value1<value2){
return 1;
}else {
return 0;
}
}
var result = compare(1,2);
EC
建立(以下说明都是对函数执行上下文的说明)
EC
的建立分为两个阶段 : 1. 进入执行上下文 2. 执行阶段
1. 进入执行上下文
发生在函数调用时,但是在执行代码之前。扫描器扫描传递给函数的参数或
arguments
,函数内的变量和函数声明,并创建执行执行上下文对象,扫描的结果将完成变量对象的创建
进入执行上下文做的事情:
- 扫描上下文环境中中声明的形参、函数以及变量,创建变量对象
VO
,并依次填充变量对象的属性
创建变量对象VO
过程
- 根据函数的形参,创建并初始化
arguments
对象:形参作为属性,实参作为其值。对于没有实参的形参,值为undefined
(这部分实际就是js的预解析机制,js的预解析机制) - 扫描函数内部,查找并完成函数声明:函数的名称作为其名,值为函数体。如果在变量对象中有同名的属性/函数,则函数体替代该属性值/覆盖函数体。
- 扫描函数内部,查找并完成变量声明:属性名为变量名,值初始化为
undefined
。如果变量名和已经存在的属性同名,不会影响到同名的值(因为都为初始值,即undefined
)。如果变量名和已经存在的函数同名,则不会替换掉函数的值,即最终该属性的值为已经存在的函数体 - 函数表达式不会替换同名属性的值。
- 求出
this
的值
2. 执行阶段
发生在函数执行时,
VO
的一些属性的undefined
值将会被确定参考:
- javascript 执行环境,变量对象,作用域链
- JS执行环境、作用域链、活动对象
- JS核心原理 - 执行环境
- 理解JS执行环境
- JS高级程序设计
- 高性能JavaScript