本文涵盖的知识点: 执行上下文和执行上下文栈,作用域和作用域链,变量对象,活动对象,this, apply, call等,请事先学习相关基础知识
首先我们写了一段代码如下,很简单对不对,执行步骤可以看到注释,下面我们就一步步分析在执行中做了什么,抓稳扶好哦!!
// -------------- 第1步 ----------------------//
var jason = "global jason";
function jasonzeng(){
var jason = "local jason";
function f(){
return jason;
}
// -------------- 第3步 ---------------//
return f();
// -------------- 第4步 --------------------//
}
// ------------------- 第2步 --------------------//
jasonzeng();
// --------------- 第5步 ---------------------//
准备阶段: 在代码执行之前,js环境会创建一个执行上下文栈的东西: ExecuteContextStack
,这个堆栈结构为真正执行做好了准备,所有执行代码需要的东西都从这个栈里面取, 因为代码还没有执行,所以现在这个栈还是空的
ExecuteContextStack = [];
第一步: 此时开始执行全局代码,此时会创建全局的执行上下文: GlobalContext
,同时全局执行上下文被压入执行上下文栈
什么时候创建执行上下文: js的只有在全局代码执行,函数执行,Eval执行的时候才会创建执行上下文, 说白了,这三种情况会创建独立的作用域,所以需要新创建上下文来保存当前环境的需要的东西
ExecuteContextStack = [
GlobalContext
]
初始化全局执行上下文: 此时GlobalContext
是什么呢,里面放的就是执行全局代码需要的东西啦,这里面有三个非常重要的东西,变量对象(Variable Object), 作用域(Scope), This, 初始化后如下, 注意这里jasonzeng
函数初始化的时内部有个属性[[scope]]
同样保存了当前函数的作用域链,这是闭包能实现的根本原因
vo, scope, this 的关系: 最重要的其实是变量对象,这里简称
VO
,VO
决定了当前执行环境里面有什么,而Scope
其实是VO
的一个从内到外的链式数组,用处就是假如在当前执行环境中需要的对象或函数时,当前VO中没有,就会顺着链条往上找这个对象或函数,而最后的this呢,则是指向正在执行的OV
, 注意this
不代表就一定指向当前上下文的OV
,比如当用call
或者apply
绑定this
到指定对象的时候,这是这个this
就指向相应对像执行上下文的OV
, 个人理解,欢迎拍砖!!
ExecuteContextStack = [
globalContext = {
VO : [
jason,
jasonzeng = {
[[scope]] : globalContext.VO
}
],
scope : [
globalContext.VO
],
this : globalContext.VO
}
];
第二步: 初始化完全局执行上下文就开始执行了,首先执行的是jasonzeng()
函数, 还记得上面说的吗? 在函数执行的时候同时会创建执行上下文jasonzengContext
,此时因为要执行函数了,里面变量对象的VO
就变成ActiveObject
, 简称AO
,因为是函数有参数,所以比较特殊,会默认创建一个arguments
的数组来保存参数,其他的变量和函数声明 类似,注意现在只是声明,没有创建, 还需要注意的是此时函数的scope
作用域链即保存了当前的AO又有上一层全局的VO, 构成完整的作用域链,当jasonzengContext
初始化完后也会压入执行上下文栈ExecuteContextStack
,现在完整的栈如下
- VO和AO的区别: 未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作, 它们其实都是同一个对象,只是处于执行上下文的不同生命周期
- 作用域链的创建: 其实就是把函数初始化的时候创建的内部变量
[[scope]]
的链复制到当前函数的执行上下文的scope上面,并且在最前端加入当前的AO构成完整的作用域链
ExecuteContextStack = [
jasonzengContext = {
AO : [
arguments = {
length : 0
},
jason = undefined,
f = function f() {}
],
Scope : [
jasonzengContext.AO , globalContext.VO
],
this : undefined
},
globalContext = {
VO : [
global,
jason,
jasonzeng = {
[[scope]] : globalContext.VO
}
],
Scope : [
globalContext.VO
],
this : globalContext.VO
}
];
第三步: 开始执行jasonzeng
函数内部代码,此时jasonzengContext
上面的AO对象会根据代码更改为相应的值,比如当执行到f()
时候,AO函数内部的jason
会被赋值为jason = "local jason",
开始执行f()
的时候跟上一步一样,初始化f函数的执行上下文fContext
,特别注意的是里面的scope
作用域链, 最后再压入执行上下文栈如下
ExecuteContextStack = [
fContext = {
AO : [
arguments = {
length : 0
}
],
Scope : [fContext.AO, jasonzengContext.AO, globalContext.VO],
this : undefined
},
jasonzengContext = {
AO : [
arguments = {
length : 0
},
jason = "local jason",
f = function f() {}
],
Scope : [
jasonzengContext.AO , globalContext.VO
],
this : jasonzengContext.AO
},
globalContext = {
VO : [
global,
jason,
jasonzeng = {
[[scope]] : globalContext.VO
}
],
Scope : [
globalContext.VO
],
this : globalContext.VO
}
];
第四步: 此时进入函数f
内部执行了,发现终于没有再内嵌函数了,所以不用再创建执行上下文了,那么执行完f
函数后它的执行上下文fContext
就肯定不用了撒,然后就从执行上下文栈中弹出,弹出后执行上下文栈如下
ExecuteContextStack = [
jasonzengContext,
globalContext
];
第五步: 同上,执行完jasonzeng
函数,执行上下文从执行上下文栈中弹出如下, 这样整个代码执行过程就讲述完了,是不是意犹未尽啊
ExecuteContextStack = [
globalContext
];
总结: 最后总结一下,我们可以看到,在整个代码执行前和执行时和执行后都在对这个执行上下文栈做操作,因为我们在执行时需要的东西都在上面,当前作用域拥有的变量可以从AO上面获取, 闭包获取的变量从作用域链上游获取,this也可以直接获取,这样很多js的各种奇葩现象都可以解释通了,这就是由里及外学习的好处呀,加油各位爱码士!!!