执行上下文(Execution Context)
每次当控制器转到可执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境。
那什么是可执行代码
- 全局代码(全局环境):你的代码首次执行的默认环境,例如加载外部的js文件或者本地标签内的代码。全局代码不包括任何function体内的代码。 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。
- 函数代码(函数环境):当函数被调用执行时,会进入当前函数中执行环境
- eval(不建议使用,可忽略)
执行上下文栈
我们都知道,浏览器中的JS解释器被实现为单线程,这实际上意味着,在浏览器中一次只会发生一件事,而在一个JavaScript程序中,必定会产生多个执行上下文,JavaScript引擎会以栈的方式来处理它们,这个栈,我们称其为执行栈(调用栈)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文,下面的这个图是单线程的栈的一个抽象的表示:
当浏览器首次载入你的脚本,它将默认进入全局执行上下文。如果,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并创建一个新的执行上下文,并将新创建的上下文压入执行栈的顶部。
如果你调用当前函数内部的其他函数,相同的事情会在此上演。代码的执行流程进入内部函数,创建一个新的执行上下文并把它压入执行栈的顶部。浏览器总会执行位于栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。
详细了解了这个过程之后,我们就可以对执行上下文总结一些结论了。
- 单线程
- 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈
- 函数的执行上下文的个数没有限制
- 每次函数被调用创建新的执行上下文,包括调用自己。
执行上下文的细节
可以将每个执行上下文抽象为一个对象并有三个属性:
executionContextObj = {
variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ },
scopeChain: { /* 变量对象(variableObject)+ 所有父执行上下文的变量对象*/ },
this: {}
}
复制代码
我们已经知道,当调用一个函数时(激活),一个新的执行上下文就会被创建。而一个执行上下文的生命周期可以分为两个阶段。
- 创建阶段【当函数被调用,但未执行任何其内部代码之前】:
- 创建变量对象(VO)。
- 创建作用域链(Scope Chain)
- 确定this指向。
- 激活/代码执行阶段:
- 会完成变量赋值,函数引用,以及执行其他代码
变量对象(VO/AO)
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。
全局上下文中的变量对象
一句话全局上下文中的变量对象就是全局对象,客户端可以通过window访问这个对象。
函数上下文中的变量对象
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
变量对象的创建,依次经历了以下几个过程(该过程是有先后顺序的:函数的形参==>>函数声明==>>变量声明)。
- 创建arguments对象。检查当前上下文中的参数,在变量对象中以形参名建立一个属性,实参作为该属性值。
- 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果变量对象已经包含了相同名字的属性,则替换它的值。
- 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
我们来看个列子详细了解下变量对象演变的一个过程
function test(a) {
var b = 1;
function c () {};
}
test(10);
// 执行上下文创建阶段
testExecutionContext = {
AO: {
arguments: {
length: 1,
0: 10,
},
a: 10,
c: pointer to Function c,
b: undefined
},
scopeChain: { ... },
this: { ... }
}
// 执行上下文执行阶段
testExecutionContext = {
AO: {
arguments: {
length: 1,
0: 10,
},
a: 10,
c: pointer to Function c,
b: 1
},
scopeChain: { ... },
this: { ... }
}
复制代码
如何理解函数声明过程中如果变量对象已经包含了相同名字的属性,则替换它的值这句话? 看如下这段代码:
function foo1(a){ console.log(a) function a(){} } foo1(20) 复制代码
根据上面的介绍,我们知道VO创建过程中,函数形参的优先级是高于函数的声明的,结果是函数体内部声明的function a(){}覆盖了函数形参a的声明,因此最后输出a是一个function
如何理解变量声明过程中如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性这句话?
//情景一:与参数名相同 function foo2(a){ console.log(a) var a = 10 } foo2(20) //情景二:与函数名相同 function foo2(a){ console.log(a) var a = 10 function a(){} } foo2(20) 复制代码