你是否曾经好奇过,JavaScript 代码是如何一步步执行的?为什么有时候变量会"提升"?函数调用是如何管理的?今天我们就来深入浅出地聊聊 JavaScript 的执行机制。
什么是执行上下文?
想象一下,你要做一道菜。在开始操作之前,你需要准备什么?
- 确认菜谱
- 准备食材
- 确定主厨
- 准备厨具
执行上下文(Execution Context)就像是这个"准备工作"的过程。当 JavaScript 代码执行进入到一个环境时,引擎会先做一些准备工作,这个准备阶段就叫做"创建执行上下文"。
简单来说:执行上下文就是代码执行前的准备工作。
具体做了什么我们后面再说,先来看下 JavaScript 执行环境有哪些?
JavaScript 中的执行环境
JavaScript 中有三种执行环境:
- 全局环境
- 函数环境
- eval 环境(不推荐使用)
对应的,就有三种执行上下文:
- 全局执行上下文 - 程序启动时创建,全局唯一
- 函数执行上下文 - 每次函数调用时创建
- eval 执行上下文 - eval 函数执行时创建
执行流程
当 JavaScript 程序运行时:
- 首先进入全局环境,创建全局执行上下文
- 遇到函数调用时,进入函数环境,创建函数执行上下文
- 多个函数调用会产生多个执行上下文
- 这些上下文通过栈的方式进行管理
理解栈数据结构
在深入执行栈之前,我们先来理解一下"栈"这个概念。
栈的特性
想象一下叠盘子的场景:

- 你只能从最上面放盘子
- 你也只能从最上面拿盘子
- 最后放上去的盘子,会最先被拿走
- 最先放的盘子,会最后被拿走
这就是栈的核心特性:后进先出(LIFO - Last In First Out)
特点 | 说明 |
---|---|
后进先出 | 最后进入的元素最先出来 |
单一出入口 | 只能从栈顶操作 |
入栈/出栈 | 放入叫入栈,取出叫出栈 |
JavaScript 执行栈(调用栈)
理解了栈的概念,我们来看看 JavaScript 是如何用栈来管理执行上下文的。
执行栈的工作原理
执行栈就像是一个"任务管理器":
- 栈底:永远是全局执行上下文
- 栈顶:当前正在执行的函数上下文
- 入栈:函数被调用时,创建新的执行上下文并推入栈顶
- 出栈:函数执行完毕,从栈顶移除其执行上下文
JavaScript 在执行代码时最先进入全局环境,而全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底。当遇到函数调用时就会进入函数执行环境,并创建函数执行上下文并将其推入栈顶,当函数调用完成后,它就会从栈顶被推出,理想的情况下,闭包会阻止该操作。
让我们通过一个具体例子来看看执行栈是如何工作的:
javascript
function foo() {
function bar() {
return 'I am bar';
}
return bar();
}
foo();
执行过程可视化:

步骤解析:
- 初始状态:只有全局执行上下文在栈中
- 调用 foo():foo 的执行上下文入栈
- 调用 bar():bar 的执行上下文入栈
- bar() 执行完:bar 的执行上下文出栈
- foo() 执行完:foo 的执行上下文出栈
- 回到全局:只剩全局执行上下文
栈溢出问题
执行栈的空间是有限的!如果函数调用层级太深,会发生栈溢出:
javascript
function foo() {
foo();
}
foo();
// 报错:Uncaught RangeError: Maximum call stack size exceeded
常见栈溢出场景:
- 无限递归
- 递归层级过深
- 忘记设置递归终止条件
执行上下文的生命周期
前面我们有说到,运行 JavaScript 代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作,接下来就我们就来看一下具体会做什么。
具体做的事就和执行上下文的生命周期有关,每个执行上下文都有自己的生命周期,分为两个主要阶段:
创建阶段
函数被调用时,会进入函数环境,为其创建一个执行上下文,此时进入创建阶段:
1. 创建变量对象(VO - Variable Object)
- 创建 Arguments 对象(并赋值)
- 确定函数参数(并赋值)
- 处理函数声明(并赋值)
- 处理变量声明(未赋值,初始为 undefined)
2. 确定 this 指向
- this 的值由调用方式决定
- 在创建阶段就已经确定
3. 确定作用域链
- 作用域由函数定义位置决定(词法作用域)
- 决定了变量的查找规则
当处于执行上下文的建立阶段时,我们可以将整个上下文环境看作是一个对象。该对象拥有 3 个属性,如下:
js
executionContextObj = {
variableObject : {}, // 变量对象,里面包含 Arguments 对象,形式参数,函数和局部变量
scopeChain : {},// 作用域链,包含内部上下文所有变量对象的列表
this : {}// 上下文中 this 的指向对象
}
这里我们重点来看一下变量对象里面所拥有的东西,在函数的建立阶段,首先会建立 Arguments 对象。然后确定形式参数,检查当前上下文中的函数声明,每找到一个函数声明,就在 variableObject 下面用函数名建立一个属性,属性值就指向该函数在内存中的地址的一个引用。最后,是确定当前上下文中的局部变量。
执行阶段
创建阶段完成后,开始执行代码:
- 变量赋值:给之前声明的变量赋予实际值
- 函数表达式赋值:处理函数表达式
- 执行代码:按顺序执行函数体中的代码
案例:
让我们通过一个例子来理解这两个阶段:
javascript
const foo = function(i) {
var a = "Hello";
var b = function privateB() {};
function c() {}
}
foo(10);
创建阶段的变量对象:
javascript
fooExecutionContext = {
variableObject: {
arguments: {0: 10, length: 1}, // Arguments 对象
i: 10, // 形参赋值
c: pointer to function c(), // 函数声明提升
a: undefined, // 变量声明,未赋值
b: undefined // 变量声明,未赋值
},
scopeChain: {},
this: {}
}
执行阶段的变量对象:
javascript
fooExecutionContext = {
variableObject: {
arguments: {0: 10, length: 1},
i: 10,
c: pointer to function c(),
a: "Hello", // 变量被赋值
b: pointer to function privateB() // 函数表达式被赋值
},
scopeChain: {},
this: {}
}
我们看到,只有在代码执行阶段,局部变量才会被赋予具体的值。在建立阶段局部变量的值都是 undefined。这其实也就解释了变量提升的原理。
现在我们再通过一个例子来加深对函数这两个阶段的过程的理解
javascript
(function () {
console.log(typeof foo); // ?
console.log(typeof bar); // ?
var foo = "Hello";
var bar = function () {
return "World";
}
function foo() {
return "good";
}
console.log(foo, typeof foo); // ?
})()
创建阶段分析:
javascript
IIFEExecutionContext = {
variableObject: {
arguments: {length: 0},
foo: pointer to function foo(), // 函数声明优先
bar: undefined // 变量声明
// 注意:同名的 var foo 被忽略了!
}
}
执行阶段结果:
javascript
(function () {
console.log(typeof foo); // "function" - 函数声明提升
console.log(typeof bar); // "undefined" - 变量提升但未赋值
var foo = "Hello"; // foo 被重新赋值为字符串
var bar = function () {
return "World";
}
function foo() {
return "good";
}
console.log(foo, typeof foo); // "Hello" "string"
})()
关键要点总结
阶段 | 主要工作 | 变量状态 | 函数状态 |
---|---|---|---|
创建阶段 | 准备工作 | 声明但未赋值(undefined) | 完全可用 |
执行阶段 | 执行代码 | 获得实际值 | 函数表达式被赋值 |
面试真题解析
Q: 谈谈你对 JavaScript 执行上下文栈的理解
A: 完整回答思路
什么是执行上下文?
执行上下文是 JavaScript 代码执行时的环境抽象概念。每当代码运行时,都是在特定的执行上下文中运行的。
执行上下文的类型
JavaScript 中有三种执行上下文:
-
**全局执行上下文:**这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事,创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
-
**函数执行上下文:**每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
-
**Eval 执行上下文:**执行在 eval 函数内部的代码也会有它属于自己的执行上下文
调用栈
调用栈是解析器(如浏览器中的的 JavaScript 解析器)的一种机制:
- 当脚本要调用一个函数时,解析器把该函数推入到栈中并且执行这个函数。
- 被这个函数中调用到的函数会进一步添加到调用栈中。
- 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
- 如果栈占用的空间比分配给它的空间还大,那么则会导致"栈溢出"错误。
希望这篇文章能帮助你更好地理解 JavaScript 的执行机制!如果你有任何问题,欢迎在评论区讨论。