本文深入解析 JavaScript 执行上下文的核心机制,涵盖全局、函数及 Eval 三种环境。详细阐述执行上下文堆栈的单线程同步执行模型,剖析建立阶段与代码执行阶段的内部流程,包括 variableObject、作用域链及 this 的确定过程,并解释变量提升与函数声明优先级的底层原因。
本文将深入探讨 JavaScript 中最基础且核心的概念——执行上下文。阅读后,你将理解 JavaScript 引擎在执行代码前的准备工作,明白为何某些函数和变量在声明前即可被访问,以及它们最终值的定义方式。
执行上下文的概念与分类
执行上下文的概念
JavaScript 中的代码运行环境主要分为以下三类:
全局级代码:这是默认的代码运行环境。代码载入后,引擎首先进入此环境。
函数级代码:当执行一个函数时,引擎会运行该函数体内的代码。
Eval 代码:在 Eval 函数内部运行的代码所对应的环境。
网络上关于作用域的阐述众多,为便于理解,此处可将“执行上下文”视为当前代码的运行环境或作用域。以下示例展示了全局与函数级执行上下文的交互:

上图展示了四个执行上下文:紫色代表全局上下文;绿色代表 person 函数内的上下文;蓝色和橙色代表 person 函数内调用的另外两个函数的上下文。需注意,无论何种情况,全局上下文始终唯一,且可被任何其他上下文访问。例如,在 person 上下文中可以访问全局上下文中的 sayHello 变量,同样,在 firstName 或 lastName 函数中也能访问该变量。
函数上下文的数量没有限制。每次调用函数时,引擎会自动创建一个新的函数上下文,即局部作用域,用于声明私有变量等。外部上下文无法直接访问该局部作用域内的元素。在上述示例中,内部函数可以访问外部上下文声明的变量,反之则不行。这背后的原因及引擎的处理机制如下:
执行上下文堆栈与单线程模型
执行上下文堆栈
在浏览器环境中,JavaScript 引擎采用单线程工作方式。这意味着某一时刻仅有一个事件被激活处理,其他事件被放入队列等待处理。下图描述了这一堆栈机制:

已知当 JavaScript 代码文件被浏览器载入后,默认首先进入全局执行上下文。当在全局上下文中调用函数时,程序流进入被调用函数,引擎为该函数创建新的执行上下文并将其压入堆栈顶部。浏览器始终执行堆栈顶部的上下文,执行完毕后将其弹出,随后执行其下的上下文。如此依次执行并弹出,直至回到全局上下文。示例如下:
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
上述 foo 函数通过 () 运算符强制直接运行。函数体调用了自身三次,每次局部变量 i 增加 1。每次 foo 函数被自身调用时,都会创建一个新的执行上下文。每当一个上下文执行完毕,它便从堆栈弹出,回到上一个上下文,直到再次回到全局上下文。整个过程抽象如下:

由此可见,执行上下文这一抽象概念可归纳为以下几点:
单线程模型
同步执行机制
存在唯一的全局上下文
函数执行上下文的数量无限制
每次调用函数时,都会为其创建一个新的执行上下文,即使是递归调用自身也是如此。
执行上下文的建立与执行阶段
执行上下文的建立过程
已知每次调用函数时都会创建新的执行上下文。然而,在 JavaScript 引擎内部,这一创建过程具体分为两个阶段:
建立阶段(发生在调用函数时,但在执行函数体具体代码之前)
建立变量、函数、arguments 对象及参数
建立作用域链
确定 this 的值
代码执行阶段:
变量赋值、函数引用及执行其他代码
实际上,可将执行上下文视为一个对象,其包含上述三个属性:
(executionContextObj = {
variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ },
scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ },
this: {}
}
建立阶段与代码执行阶段的详细分析
确切地说,执行上下文对象(即上述 executionContextObj)是在函数被调用时、但在函数体真正执行前创建的。函数被调用即指前述两个阶段中的第一阶段——建立阶段。此时,引擎会检查函数中的参数、声明的变量及内部函数,并基于这些信息建立执行上下文对象。在此阶段,variableObject 对象、作用域链以及 this 指向的对象均会被确定。
上述第一阶段的具体过程如下:
定位当前上下文中调用函数的代码
在被调用函数体代码执行前,开始创建执行上下文
进入第一阶段——建立阶段:
建立 variableObject 对象:
建立 arguments 对象,检查当前上下文中的参数,建立该对象下的属性及属性值
检查当前上下文中的函数声明:
每找到一个函数声明,就在 variableObject 下以函数名建立属性,属性值为指向该函数在内存中地址的引用
若上述函数名已存在于 variableObject 下,则对应属性值会被新引用覆盖。
初始化作用域链
确定上下文中 this 指向的对象
代码执行阶段:
执行函数体中的代码,逐行运行,给 variableObject 中的变量属性赋值。
以下是一个具体的代码示例:
function foo(i) {
var a = 'hello';
var b = function privateB() {
};
function c() {
}
}
foo(22);
在调用 foo(22) 时,建立阶段如下:
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
scopeChain: { ... },
this: { ... }
}
由此可见,在建立阶段,除 arguments、函数声明及参数被赋予具体属性值外,其他变量属性默认均为 undefined。一旦建立阶段结束,引擎即进入代码执行阶段。该阶段完成后,上述执行上下文对象如下:
fooExecutionContext = {
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
scopeChain: { ... },
this: { ... }
}
我们看到,仅在代码执行阶段,变量属性才会被赋予具体值。
变量提升与作用域机制解析
局部变量作用域提升的原因
网上常有总结称:在函数中声明的变量及函数,其作用域提升至函数顶部,即一进入函数体即可访问其中声明的变量及函数。这是正确的,但知其然是否知其所以然?相信通过上述解释你已有所明白,此处再作分析。
看以下代码:
(function() {
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
}());
上述代码定义了一个匿名函数,并通过 () 运算符强制立即执行。此时会创建一个执行上下文。我们看到示例中可立即访问 foo 及 bar 变量,并通过 typeof 输出 foo 为函数引用,bar 为 undefined。
为何我们能在声明 foo 变量前访问到它?
因为在上下文的建立阶段,先处理 arguments、参数,接着处理函数声明,最后处理变量声明。发现 foo 函数声明后,便在 variableObject 下建立 foo 属性,其值为指向函数的引用。处理变量声明时,发现 var foo 声明,但 variableObject 已具有 foo 属性,故直接跳过。进入代码执行阶段时,即可通过访问 foo 属性,因为它已存在且为函数引用。
为何 bar 是 undefined?
因为 bar 是变量声明,在建立阶段被赋予默认值 undefined。由于它仅在代码执行阶段才被赋予具体值,所以当调用 typeof(bar) 时输出值为 undefined。
至此,相信你对执行上下文已有充分理解。该概念至关重要,务必深入掌握!
以上关于 JavaScript 执行上下文的全面解析即为全部内容,希望能对大家有所帮助,也希望大家多多支持本站。