在 JavaScript 学习中,变量提升、作用域屏蔽等问题常常让初学者困惑。比如一段看似简单的代码,却能引出关于执行机制的深层思考。本文将以如下代码为例,从执行上下文和调用栈的底层视角等等,完整拆解代码的执行流程,带你看透 JS 代码运行的核心逻辑。
一、JS 是如何执行的?
在 Chrome 浏览器中,JavaScript 的执行由 V8 引擎 负责。
V8 在运行 JS 代码时分为两个阶段:
1️⃣ 编译阶段
在代码执行前的一刹那,V8 会:
工作内容:
-
语法分析
检查语法错误(比如括号、花括号是否配对)。
-
变量提升(Hoisting)
var声明的变量 → 提前创建并赋值为undefined- 函数声明(
function xxx(){}) → 整体提升(优先级最高)
-
创建执行上下文对象 (Execution Context Object)
-
包含三部分:
- 变量环境
- 词法环境
- 可执行代码
-
-
把执行上下文压入调用栈 (Call Stack)
- 全局上下文 → 首先压栈
- 函数被调用 → 创建新的函数上下文 → 压栈
2️⃣ 执行阶段
编译完后开始执行:
- 变量和函数声明已准备好
- 按代码顺序逐行执行
- 函数调用 → 创建新上下文 → 压栈
- 函数执行完毕 → 上下文出栈(销毁,等待垃圾回收)
二、执行上下文与调用栈
V8 通过一个叫做 调用栈(Call Stack) 的结构来管理代码执行过程。
我们可以把它想象成一个「任务清单」:
- 全局执行上下文(Global Execution Context) 首先创建并压入栈底;
- 当执行函数时,会创建一个新的函数执行上下文,并压入栈顶;
- 函数执行完毕后,从栈顶弹出(出栈);
- 栈顶总是代表当前正在执行的上下文。
JS 引擎启动后,会自动创建一个 全局执行上下文。
此时,执行栈中只有它一个上下文:
┌────────────────────┐ ← 栈顶
│ 全局执行上下文 │
└────────────────────┘ ← 栈底
✅ 所以,在创建全局执行上下文时,它既是第一个入栈的 ,
也是当前栈顶的上下文。
javascript
var a = 1;
function fn(a) {
var a = 2;
function a() {}
var b = a;
console.log(a);
}
fn(3);
调用栈变化示意:
| 阶段 | 栈顶内容 | 说明 |
|---|---|---|
| 初始 | 全局上下文 | 代码准备执行 |
| 调用 fn(3) | fn 执行上下文 | 函数被调用,压入栈顶 |
| 执行完 fn | 全局上下文 | 函数上下文出栈 |
| 程序结束 | 全局上下文销毁 | 页面关闭或脚本结束 |
① 程序开始 → 创建全局执行上下文
css
[ 全局执行上下文 ]
变量环境: { a: undefined, fn: <function> }
词法环境: {}
代码: 全局代码
执行到 a = 1; fn(3); 时:
| 名称 | 值 |
|---|---|
| a | 1 |
| fn | function |
② 调用 fn(3) → 创建新的函数执行上下文
php
┌────────────────────┐ ← 栈顶(当前执行环境)
│ fn 执行上下文 │
├────────────────────┤
│ 全局执行上下文 │ ← 栈底
└────────────────────┘
JS 引擎调用函数 fn,于是创建 fn 的执行上下文,并压入栈顶:
此时:
-
全局还在栈中(没被销毁);
-
但栈顶变成了
fn; -
JS 正在执行
fn函数体的代码。
编译阶段:
逐步提升分析:
-
形参
a→ 先在环境中占位inia = 3 (调用时传入的参数) -
发现函数声明
function a() {}⇒ 提升并覆盖前面的 a
csharpa = function a() {} -
发现
var a = 2;⇒
var a部分已存在(被提升过),此时不会再声明,只会在执行阶段再赋值。 -
发现
var b;⇒
b = undefined
编译阶段结束后:
| 名称 | 值 |
|---|---|
| a | function a() {} |
| b | undefined |
css
fn 执行上下文
变量环境:
a: function a(){} // 函数声明覆盖形参
b: undefined
词法环境:
(空)
代码:
var a = 2;
function a() {}
var b = a;
console.log(a);
执行阶段:
var a = 2;→ a = 2(覆盖变量环境中的 a: function a(){})var b = a;→ b = 2console.log(a);→ 输出2
然后函数执行完毕 → 出栈。
③ 回到全局上下文
调用栈恢复为:全局执行上下文
php
执行栈状态:
┌────────────────────────┐
│ fn 函数执行上下文 │ ← 出栈(弹出)
├────────────────────────┤
│ 全局执行上下文 │ ← 回到全局
└────────────────────────┘
最终执行栈:
┌────────────────────────┐
│ 全局执行上下文 │
└────────────────────────┘
程序执行结束。
三、函数表达式不会被提升
我们来看一个非常经典的坑:
javascript
func(); // ❌ ReferenceError
let func = () => {
console.log('函数表达式不会提升');
}
1️⃣ 编译阶段:
- 变量
func被登记进 词法环境; - 但由于是
let声明,它尚未初始化; - 此时
func处于 暂时性死区(TDZ) 。
2️⃣ 执行阶段:
-
执行到
func();时,JS 发现func尚未初始化; -
于是抛出:
javascriptReferenceError: Cannot access 'func' before initialization
对比 var:
go
func(); // ❌ TypeError: func is not a function
var func = function() {}
var提升会使func被初始化为undefined;- 调用时相当于
undefined(); - 所以报的是
TypeError。
✅ 结论:
let/const存在暂时性死区;var会变量提升。
四、严格模式下的执行机制
javascript
'use strict';
var a = 1;
var a = 2;
许多人以为"严格模式会禁止重复声明",但其实不然。
严格模式下:
var依然允许重复声明;- 只是禁止未声明变量直接使用;
- 禁止 this 自动绑定到全局对象;
- 禁止删除变量;
- 禁止函数参数重名等。
所以上面的代码仍然能正常执行,最终 a = 2。
只有
let和const声明时,重复定义才会抛出错误。
五、拓展:严格模式的其他影响
| 特性 | 普通模式 | 严格模式 |
|---|---|---|
| 未声明直接赋值 | 自动创建全局变量 | ❌ 报错 |
| 重复声明 var | ✅ 允许 | ✅ 允许 |
| 重复声明 let/const | ❌ 报错 | ❌ 报错 |
| this 指向 | 全局对象(window) | undefined |
| 删除变量 | 静默失败 | ❌ 报错 |
| 函数参数重名 | ✅ 允许 | ❌ 报错 |
六、JS 底层机制(内存):值类型与引用类型详解
ini
// 基本数据类型(Number):存储在栈内存中
let num = 1;
// 引用数据类型(Object):栈内存存储引用地址,堆内存存储实际对象
let obj = { age: 18 };

1.简单数据类型
javascript
let num1 = 10;
let num2 = 20;
num1 = num2;
console.log(num1);
1️⃣ 编译阶段
- JS 引擎在栈内存中为
num1、num2各分配一块空间; - 它们都属于简单数据类型(number) ;
- 值直接存在栈中。
2️⃣ 执行阶段
ini
num1 = num2;
这一步只是把 num2 的值 20 拷贝一份赋给 num1,
它们之间完全没有引用关系。
2.复杂数据类型
javascript
let obj1 = {age:18};
let obj2 = obj1;
console.log(obj2);

1️⃣ 编译阶段
JavaScript 引擎在栈内存中登记两个变量名:
javascript
obj1 → undefined
obj2 → undefined
(此时只是变量声明,还未赋值)
2️⃣ 执行阶段
开始一行行执行代码👇
ini
let obj1 = { age: 18 };
- 在堆内存 中创建一个对象
{ age: 18 }; - 假设它在堆内存中的地址是
0x12312; - 然后在栈中保存
obj1 → 0x12312(也就是对象的引用地址)。
当前内存图:
css
栈内存:
obj1 → 0x12312
堆内存:
0x12312 → { age: 18 }
javascript
let obj2 = obj1;
并不会在堆中创建新对象;
只是把 obj1 的地址拷贝一份给 obj2;
所以现在两个变量都指向同一个堆内存对象。
内存示意图:
css
栈内存:
obj1 → 0x12312
obj2 → 0x12312
堆内存:
0x12312 → { age: 18 }
javascript
console.log(obj2);
- 输出
obj2当前指向的对象,即堆内存中地址0x001里的数据; - 结果:
{ age: 18 }
🚨七、 JS 执行机制与内存总结
1️⃣ 执行机制
- JS 由 V8 引擎 执行,分为 编译阶段 和 执行阶段。
- 编译阶段:创建执行上下文、变量提升、语法检查。
- 执行阶段 :按顺序执行代码,遇到函数会创建新的执行上下文压入调用栈。
- 函数执行完毕后,执行上下文从栈中弹出(退栈,释放内存)。
2️⃣ 数据类型与内存
| 类型 | 存储位置 | 保存内容 | 拷贝方式 | 是否共享 |
|---|---|---|---|---|
| 简单类型(Number、String、Boolean、null、undefined、Symbol、BigInt) | 栈 | 值 | 值拷贝 | ❌ 否 |
| 复杂类型(Object、Array、Function) | 栈 + 堆 | 地址 | 引用拷贝 | ✅ 是 |
🔍参考文档:mdn