在前端开发的学习与工作中,JavaScript 的执行机制是核心底层原理之一。无论是解决变量提升、作用域、闭包问题,还是排查代码执行顺序的 bug,都离不开对 JS 执行机制的透彻理解。很多开发者在编写代码时,会遇到let和var声明变量的差异、函数调用顺序异常、变量未定义报错等问题,本质上都是对 JS 的执行上下文、编译执行流程、调用栈等核心概念认知不足。本文将从执行上下文、编译过程、调用栈、let/const特性四个维度,结合代码示例,全面拆解 JavaScript 的执行机制。
一、JavaScript 执行的核心:执行上下文
当 JavaScript 引擎执行代码时,并非直接逐行运行,而是先创建一个执行上下文(Execution Context),这是 JS 代码执行的环境容器,所有代码的执行都依赖于这个对象。执行上下文内部包含了代码执行所需的所有变量、函数、作用域信息,我们可以把它理解为代码运行的 "工作台"。
执行上下文分为三种类型:
- 全局执行上下文:默认的基础上下文,任何不在函数内部的代码都在全局上下文中执行,一个程序只会存在一个全局执行上下文;
- 函数执行上下文:每当函数被调用时,就会为该函数创建一个独立的执行上下文;
- eval 执行上下文 :使用
eval函数时创建,日常开发中极少使用,本文不做重点讲解。
执行上下文对象内部由三个核心部分组成,这也是 JS 执行机制的基础:
1. 变量环境(Variable Environment)
变量环境专门存储var声明的变量和function声明的函数。在 JS 编译阶段,引擎会扫描代码,将var声明的变量、函数声明提前存入变量环境,这也是变量提升 和函数提升的根源。
2. 词法环境(Lexical Environment)
词法环境用于存储let和const声明的变量,它支持块级作用域,这是与变量环境最核心的区别。词法环境采用栈结构管理不同块级作用域的变量,保证了变量的作用域隔离。
3. 执行的代码
这是执行上下文中的可执行代码段,编译完成后,JS 引擎会从上到下顺序执行这段代码。
简单来说,变量环境、词法环境、执行代码三者共同构成了执行上下文对象,所有 JS 代码的运行都依托于这个核心对象。
二、先编译,后执行:JS 代码的执行流程
很多初学者会误以为 JS 是 "边编译边执行" 的解释型语言,实际上,JavaScript 的执行遵循「编译阶段 → 执行阶段」的固定流程,编译永远发生在代码执行的前一刻。无论全局代码还是函数体代码,执行前都会先完成编译,生成执行上下文。
编译阶段的完整过程
编译阶段的核心工作是创建执行上下文,并完成变量、函数、形参的声明处理,具体分为四步:
- 创建执行上下文对象:为当前代码(全局 / 函数)初始化执行上下文;
- 扫描变量与形参声明 :找到代码中
var声明的变量、函数形参,将变量名 / 形参名作为执行上下文的 key,值默认赋值为undefined; - 统一形参与实参的值:仅函数执行上下文有此步骤,将调用函数时传入的实参赋值给对应的形参,全局上下文无参数,跳过该步骤;
- 扫描函数声明 :找到
function关键字声明的函数,将函数名作为 key,函数体作为值存入执行上下文,函数声明会覆盖同名的变量 / 形参。
编译阶段只做声明,不做赋值 ,赋值操作会在执行阶段完成。这就是为什么var声明的变量在声明前访问会得到undefined,而不是报错。
代码示例:编译与执行的实际表现
javascript
运行
javascript
// 全局代码编译 → 执行
console.log(num); // 输出:undefined(变量提升)
var num = 100; // 编译阶段声明num,执行阶段赋值100
// 函数声明提升
fn(); // 输出:函数执行成功(函数提升优先于变量提升)
function fn() {
console.log("函数执行成功");
}
// 对比:函数表达式不会提升
console.log(foo); // 输出:undefined
var foo = function() {
console.log("函数表达式");
};
foo(); // 输出:函数表达式
代码解析:
- 编译阶段:JS 引擎扫描到
var num、var foo、function fn(),将num、foo存入变量环境,值为undefined,将fn存入变量环境,值为函数体; - 执行阶段:逐行执行代码,先打印
num,此时仅声明未赋值,输出undefined;随后赋值num=100;调用fn()时,函数已提升,可正常执行;foo是函数表达式,编译阶段仅声明变量,执行阶段才赋值函数,因此声明前打印为undefined。
这个示例清晰体现了 JS「先编译、后执行」的核心流程,也解释了变量提升和函数提升的底层原因。
三、调用栈:管理函数调用的核心数据结构
JS 引擎(V8)内部使用调用栈(Call Stack) 来管理函数之间的调用关系,调用栈是一种遵循「后进先出(LIFO)」原则的栈结构,专门用于存储代码运行时创建的所有执行上下文。
调用栈的工作机制
- 代码开始执行时,首先创建全局执行上下文,压入调用栈底部,栈顶指针指向全局上下文,代表当前正在执行全局代码;
- 当调用一个函数时,引擎会为该函数创建函数执行上下文,压入调用栈顶部,栈顶指针切换到该函数上下文,开始执行函数内部代码;
- 函数执行完毕后,其执行上下文会从调用栈弹出(销毁),栈顶指针回到上一个上下文(全局或父函数),继续执行剩余代码;
- 整个程序执行结束后,全局执行上下文弹出调用栈,栈为空。
代码示例:调用栈的执行过程
javascript
运行
javascript
// 全局执行上下文入栈
console.log("全局代码开始执行");
// 定义函数
function func1() {
console.log("func1执行");
func2(); // 调用func2
}
function func2() {
console.log("func2执行");
}
func1(); // 调用func1
console.log("全局代码执行完毕");
调用栈执行流程:
- 全局执行上下文入栈,执行
全局代码开始执行; - 调用
func1(),func1执行上下文入栈(栈顶),执行func1执行; func1内部调用func2(),func2执行上下文入栈(栈顶),执行func2执行;func2执行完毕,出栈销毁,回到func1;func1执行完毕,出栈销毁,回到全局上下文;- 执行
全局代码执行完毕,全局上下文出栈,程序结束。
调用栈的作用不仅是管理执行顺序,还能帮助我们排查栈溢出 问题:当函数无限递归调用自身时,执行上下文会不断压入栈,超出栈的最大容量,就会触发Maximum call stack size exceeded错误。
javascript
运行
scss
// 递归无终止条件,栈溢出
function recursion() {
recursion();
}
recursion(); // 报错:Maximum call stack size exceeded
四、let 和 const:词法环境与块级作用域
ES6 新增的let和const声明方式,解决了传统var的诸多缺陷,它们的底层存储在词法环境中,拥有独立的作用域规则,是 JS 执行机制的重要优化。
1. 核心特性:块级作用域
var不支持块级作用域,在{}(if、for、while 等代码块)内声明的var变量,会泄露到全局;而let/const支持块级作用域,变量仅在当前代码块内有效,外部无法访问。
javascript
运行
javascript
// var 无块级作用域
if (true) {
var num1 = 10;
}
console.log(num1); // 输出:10(变量泄露)
// let 有块级作用域
if (true) {
let num2 = 20;
}
console.log(num2); // 报错:num2 is not defined
2. 不存在变量提升:暂时性死区
var的变量提升会导致代码逻辑混乱,而let/const不存在变量提升 ,从代码块开始到变量声明之前,该变量处于暂时性死区(TDZ),访问会直接报错。
javascript
运行
ini
// 暂时性死区
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 100;
// var 对比
console.log(b); // 输出:undefined
var b = 200;
3. 不可重复声明
var允许重复声明变量,后面的声明会覆盖前面的;let/const不允许在同一作用域内重复声明,否则直接报错。
javascript
运行
ini
// var 重复声明,无报错
var name = "张三";
var name = "李四";
console.log(name); // 输出:李四
// let 重复声明,报错
let age = 18;
let age = 20; // 报错:Identifier 'age' has already been declared
4. const 常量必须初始化且不可修改
const用于声明常量,声明时必须赋值,且赋值后不能修改基础数据类型;如果声明引用数据类型(对象 / 数组),可以修改内部属性,但不能修改引用地址。
javascript
运行
ini
// const 必须初始化
const PI; // 报错:Missing initializer in const declaration
// 基础数据类型不可修改
const PI = 3.14;
PI = 3.1415; // 报错:Assignment to constant variable
// 引用数据类型:可修改内部属性,不可修改地址
const obj = { name: "张三" };
obj.name = "李四"; // 正常执行
obj = {}; // 报错:Assignment to constant variable
let/const的这些特性,本质上是因为它们存储在词法环境中,词法环境通过栈结构严格管理块级作用域的变量生命周期,规避了var的设计缺陷,让 JS 的变量管理更加规范。
五、JavaScript 执行机制全流程总结
结合以上所有核心知识点,我们可以梳理出 JavaScript 完整的执行流程:
-
代码读取:JS 引擎读取待执行的代码;
-
编译阶段:
- 创建执行上下文对象(全局 / 函数);
- 变量环境存入
var变量、函数声明,赋值undefined/ 函数体; - 词法环境记录
let/const变量声明(不赋值); - 函数上下文统一形参和实参;
-
执行阶段:执行上下文存入调用栈,栈顶指针指向当前执行上下文,代码从上到下顺序执行,完成变量赋值、函数调用等操作;
-
上下文销毁:函数执行完毕,上下文弹出调用栈并销毁;程序结束,全局上下文销毁。
调用栈是执行上下文的容器,遵循后进先出原则;变量环境管理var/function,词法环境管理let/const,二者共同构成了执行上下文的变量存储体系。
理解 JS 执行机制,是掌握作用域、闭包、异步任务、事件循环等高级知识点的基础。无论是日常开发中的代码调试,还是面试中的底层原理考察,透彻理解执行上下文、编译执行、调用栈、let/const特性,都能让我们写出更规范、更健壮的 JavaScript 代码。