在 javascript中每一个函数在执行时都会创建一个独属于这个函数的执行上下文
,其中存储了该函数创建的,可访问的变量,函数内部 this 指向等等。在函数执行完毕之后执行上下文会被销毁。
本文分析函数创建到被调用结束,执行上下文被创建,压入栈,之后被销毁这个过程,由此引出函数作用域,变量对象等概念。该文章中大部分代码为伪代码,只是意图说明某种结构或者概念。
全局环境
Javascript 中当代码开始执行时,会首先创建一个全局的执行上下文,全局环境中的变量都存储在该执行上下文中。同时为了便于管理,这个全局的执行上下文会被推入执行上下文栈。在 javascript 运行过程中,全局执行上下文始终保持在栈底,直到程序运行结束,才会被推出并销毁。
函数环境
当一个被声明的函数被调用时,会创建独属于这个函数的执行上下文,并且将该执行上下文推入执行上下文栈,函数调用结束后,函数对应的执行上下文被推出并销毁。要注意的是在这个函数中如果调用了另外的函数B,则又会创建这个函数 B 的执行上下文,并推入栈。
个人理解,执行上下文相当于一个函数执行的环境,javascript 将其存储在执行上下文栈中,在这个环境中保存了该函数可以访问的变量,以供函数运行时调用。而如果我们在函数中不断地创建新的函数,调用函数,则执行上下文栈会不断被推入上下文,如果调用次数过多,超过了堆栈可容纳的大小,则会造成堆栈溢出。这就是为什么有些递归次数过多的程序会发生堆栈溢出的问题。
执行上下文
那么执行上下文中究包含一些什么东西呢?
执行上下文中主要包含:
context: {
VO, // 变量对象
scope, // 函数作用域链
this, // 函数 this 指向
}
下面我们对这里前两项的内容进行详细的解释。由于我们判断 this 指向问题,一般由调用时判断,与执行上下文创建过程关系不大,因此本文暂不对 this 进行解释,后续会单独出 this 相关的文章。
VO
指变量对象(Variable Object),其中存储函数内部声明的一些变量,方法,函数的参数等。
与变量对象相对的是活动对象(Activation Object)。这两者其实本质是一样的,都是用以存储函数内部属性。区别在于,在函数预编译过程中,就是函数被调用的准备阶段,这个对象是变量对象,其属性不可访问;在函数执行阶段,该对象变为活动对象,其上的属性可以进行访问。
变量对象的内容一般如下:
VO = {
arguments: {}, // 参数
a, // 声明的变量
c: reference to function c, // 声明的函数
}
例如,对于
function a(x) {
var y = 20;
var w = function(){ ... }
function z(){ ... }
}
a(2);
在 a 函数运行的准备阶段,它的执行上下文的 VO 是:
VO = {
arguments: {
0: 2,
length: 1,
},
x: 2,
y: undefined,
w: undefined,
z: reference to function z,
}
这里有一个规则,在一个函数被调用,它的执行上下文的创建阶段,对应 VO 的创建阶段。相当于函数还未运行时 VO 的初始化,这个过程中,顺序 ”运行“ 函数内的代码,但是仅进行变量的声明而非赋值,对于参数和函数声明例外。
如上图代码所示,参数 x 及函数 z 此时已经有了值,而 y 变量和使用变量声明的函数 w ,则为 undefined.
要注意,如果该函数调用时未传入实参,则 x 应为 undefined.
OK, 当函数准备完执行前该准备的一切,此时进入执行阶段,VO 变为 AO,并且函数真正意义上地开始顺序执行。这个过程包括执行输出语句,对进行了赋值的变量依次填充内容,调用函数内部声明的函数,进而创建另外一个执行上下文等等。
AO = {
arguments: {
0: 2,
length: 1,
},
x: 2,
y: 20,
w: reference to FunctionExpression "d",
z: reference to function z,
}
函数从调用的准备阶段到真正的执行阶段,函数执行上下文中 VO的创建与执行,这个过程可以解释一些问题,例如变量提升,函数提升等。
scope
指作用域链,一个函数在运行期间,执行上下文的作用域链 = 函数外部环境变量作用域链 + 自身 AO
在函数创建时,这个函数的 [[scope]] 属性被创建,这个属性包含了它被创建时的外部环境作用域链,例如在全局环境中创建一个函数:
function t() { ... }
在它创建时,它的[[scope]]包含了全局环境作用域,实际上每个环境的作用域由它的变量对象体现。因此
t.[[scope]] = {
globalContext.VO
}
如果此时运行t函数,则开始上面讲过的执行上下文,VO 创建过程,同时在 t 函数运行前的准备阶段,将会复制它的[[scope]]用于创建作用域链,并将当前的 VO/AO 推入作用域链顶层,相当于:
scope = {
t.AO,
t.[[scope]],
}
相当于:
tContext = {
AO: ...,
scope = {
tContext.AO,
globalContext.VO,
}
}
同理,如果 t 函数中创建一个 f 函数,该函数的[[scope]]将包含 t 函数环境的作用域链:
function t(){
function f(){ ... }
}
f.[[scope]] = {
tContext.VO,
globalContext.VO,
}
在 f 函数运行时,再将自己的 VO/AO 推入顶层
fContext = {
AO: ...,
scope = {
fContext.AO,
tContext.VO,
globalContext.VO,
}
}
这样就解释了函数作用域链的问题,函数中调用的变量,会在当前最顶层的自己的作用域中寻找,如果没有的话沿着作用域链向上进行查找,可以一直找到全局作用域。也正因为只有函数的创建和执行会进行这样的过程,因此 Javascript 中只有函数作用域,而没有块作用域。
概念解释到目前已经完成,下面我们针对两道题目来进行分析。
题目一
var x = 21;
var talk = function () {
console.log(x);
var x = 20;
};
talk (); // undefined
该题目可以用变量提升来解释,我们使用上面执行上下文创建这个过程来演示一遍。
首先在全局环境中声明 x 变量并赋值,声明 talk 变量并将一个函数的引用赋予该变量。talk 函数在创建时,其[[scope]]属性包含了 talk 函数声明时的环境作用域链,当前仅有全局作用域:
talk.[[scope]] = {
globalContext,
}
talk 被调用时,有一个函数执行前准备阶段,该阶段创建 talk的执行上下文,并将其压入执行上下文栈顶部,其中包含了 VO, 和从 talk.[[scope]] 复制的作用域链,并且复制作用域链之后,将自身VO 推入链顶部。
在函数执行的准备阶段,对函数中声明的变量进行声明工作,不进行赋值。
准备阶段结束后函数上下文主要内容如下:
talkContext = {
VO: {
arguments: {
length: 0,
}
x: undefined
}
scope: {
VO,
globalContext,
}
}
开始执行 talk 函数:进行变量赋值等其他操作,顺序执行。
执行到console.log(x);
行时,由于此时 talkContext 中 x 值为 undefined,所以输出 undefined。
输出后继续执行,x 被赋值为 20,执行上下文:
talkContext = {
VO: {
arguments: {
length: 0,
}
x: 20
}
scope: {
VO,
globalContext,
}
}
函数执行完毕,talkContext 从上下文栈中被推出,并销毁。
题目二
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
该题目可以使用静态作用域来解释,即 javascript 中的函数作用域为静态作用域,在函数创建时决定。同样,也可以使用之前的执行上下文中的作用域链来解释。
分析一下,在 foo 函数创建时:
foo.[[scope]] = {
globalContext
}
bar 函数的创建和其执行上下文的创建过程我不再一一列出,我们关心 foo 函数被调用时,发生了什么。
foo 函数执行前准备时,创建了它的执行上下文,并推入了栈中:
fooContext = {
VO: {
arguments: {
length: 0,
}
},
scope: {
VO,
globalContext,
}
}
foo 函数的作用域链中,除了自己的 VO,就是从[[scope]]属性复制的创建时环境中作用域链,因此,只有 VO 和 globalContext,打印 value 时,延作用域链向上寻找,第一层是当前作用域 VO,没有声明 value 变量,继续向上查找,第二层是全局作用域,定义 value 值为 1,进行打印。
总结
理清 Javascript 运行时执行上下文的创建,初始化和执行这个过程有助于我们理解代码实际的运行结果。可能往往编程中会遇到一些实际执行结果和预期结果不同的状况,这种时候靠这种分析手段,我们可以更好地理解代码的运行,甚至规避一些可能出现的问题。
本文主要是分析了执行上下文
的创建和销毁中间的过程,提到了执行上下文中最主要的部分,即:作用域链
和变量对象
,以及他们的用途。
在执行上下文中另外有记录当前函数内 this 的指向,这一概念我们会再后续的文章中进行总结。另外,Javascript 有延长作用域的功能,以起到简化代码的功能;还有闭包的产生也打破了函数内部的变量只能内部进行访问的规则,那么他们对于执行上下文究竟有什么影响,这一部分后续我们也将会有文章来进行分析和说明。
本文参考资源如下:
JavaScript高级程序设计(第3版)4.2 执行环境及作用域
冴羽的博客 JavaScript深入之执行上下文
高性能 javascript 2.1 管理作用域