🔥 从内存到执行:彻底搞懂 JS 变量、作用域与引擎底层机制(附图解)
“为什么改了 obj2,obj 也变了?”
“var 和 let 到底差在哪?”
“函数提升到底是怎么工作的?”如果你被这些问题困扰过,恭喜你——今天这篇文章将用最直观的方式,带你穿透 JavaScript 表面语法,直击 V8 引擎的内存模型 + 执行机制。
全文结合真实代码 + 内存图解 + 编译流程拆解,新手也能秒懂!建议收藏 ✨
🧩 一、一个经典面试题,暴露你的知识盲区
先看这段代码,猜猜输出什么?
var a = 1;
function fn() {
console.log(a);
var a = 2;
function a() {}
var b = a;
console.log(a);
}
fn();
❓ 输出是:
- A)
1,2- B)
undefined,2- C)
[Function: a],2- D) 报错!
如果你犹豫了,说明你还没真正理解 JS 的编译机制 + 变量提升 + 函数优先级。
别急,我们一步步拆解。
🧠 二、JS 不是“解释执行”!它有编译阶段
很多人以为 JS 是“边解释边执行”的脚本语言,大错特错!
现代 JS 引擎(如 V8)采用 “即时编译”(JIT),在执行前会经历两个关键阶段:
✅ 阶段 1:编译阶段(Compilation)
- 检查语法错误
- 变量提升(Hoisting)
- 构建执行上下文(Execution Context)
- 生成字节码
✅ 阶段 2:执行阶段(Execution)
- 按顺序执行可执行代码
- 赋值、调用函数、操作 DOM
💡 关键:
“提升”发生在编译阶段,赋值发生在执行阶段!
🏗️ 三、执行上下文:JS 执行的“沙盒”
每当 JS 运行一段代码,V8 会创建一个 执行上下文(Context),包含:
| 组成部分 | 作用 |
|---|---|
| 变量环境(Variable Environment) | 存放 var 声明、函数声明 |
| 词法环境(Lexical Environment) | 存放 let/const(带 TDZ) |
| this 绑定 | 确定 this 指向 |
| 外层引用 | 用于作用域链查找 |
所有执行上下文通过 调用栈(Call Stack) 管理:
- 全局上下文最先入栈
- 函数调用 → 新上下文入栈
- 函数结束 → 上下文出栈 + 内存回收
🔍 四、回到开头的代码:逐帧解析
var a = 1;
function fn() {
console.log(a);
var a = 2;
function a() {}
var b = a;
console.log(a);
}
fn();
📌 编译阶段(fn 函数内部)
V8 扫描函数体,按优先级处理声明:
- 函数声明
function a() {}→ 提升,a = function a() {} var a→ 提升,但不覆盖函数声明(函数优先级更高)var b→ 提升为undefined
⚠️ 注意:虽然
var a和函数同名,但函数声明优先于变量声明!
此时 fn 的变量环境为:
{
a: function a() {},
b: undefined
}
📌 执行阶段
console.log(a)→ 输出function a() {}var a = 2→ 赋值,a从函数变为数字2var b = a→b = 2console.log(a)→ 输出2
✅ 最终输出:
[Function: a]
2
🎯 答案:C
💾 五、内存模型:为什么 obj2 改了 obj 也变?
再看这段代码:
let obj = { name: '郑老板', age: 18 };
let obj2 = obj;
obj2.age++;
console.log(obj.age); // 19!
核心原因:原始类型 vs 引用类型 的内存存储方式不同
✅ 原始类型(string, number, boolean...)→ 栈内存
- 值直接存储
- 赋值 = 复制值
let str = 'hello';
let str2 = str; // 栈中复制一份 "hello"
str2 = '你好'; // str2 指向新值,str 不变
内存图:
栈:
┌──────────┐
│ str: "hello" │
├──────────┤
│ str2: "你好" │ ← 独立副本
└──────────┘
✅ 引用类型(object, array, function)→ 堆内存 + 栈存地址
- 实际数据存在堆
- 变量存的是指向堆的指针(引用)
let obj = { age: 18 };
let obj2 = obj; // 栈中复制的是“地址”,不是对象本身
obj2.age++; // 通过地址修改堆中的同一个对象
内存图:
栈: 堆:
┌───────┐ ┌──────────────────┐
│ obj ──┼────────▶│ { age: 18 → 19 } │
├───────┤ └──────────────────┘
│ obj2 ─┼─────────┘
└───────┘
💡 记住:
“赋值对象 = 复制钥匙,不是复制房子”
🆚 六、var vs let:不只是作用域的区别
| 特性 | var | let |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 提升行为 | 提升 + 初始化为 undefined | 提升 + 暂时性死区(TDZ) |
| 重复声明 | 允许 | 禁止 |
| 全局属性 | 挂载到 window | 不挂载 |
🌰 TDZ 示例:
console.log(x); // ReferenceError!
let x = 10;
而 var:
console.log(y); // undefined(危险!)
var y = 20;
✅ 最佳实践:
永远用const/let,彻底告别var!
🚀 七、性能与优化启示
理解这些机制,能帮你写出更高效的代码:
-
避免不必要的对象共享
→ 使用深拷贝(structuredClone或lodash.cloneDeep) -
减少全局变量
→ 全局变量长期驻留内存,不易 GC -
利用块级作用域
→let/const在块结束后可被回收 -
函数声明优于函数表达式
→ 提升更彻底,可提前调用
✅ 八、总结:一张图掌握 JS 执行全貌
[ 代码 ]
↓
[ 编译阶段 ] → 变量提升 / 函数提升 / 创建执行上下文
↓
[ 执行阶段 ] → 赋值 / 调用 / 修改
↓
[ 内存模型 ]
├─ 原始类型 → 栈(值拷贝)
└─ 引用类型 → 堆(引用拷贝)
↓
[ 调用栈管理 ] → 入栈 → 执行 → 出栈 → GC
🌟 终极口诀:
“编译提升,执行赋值;
栈存简单,堆存复杂;
var 危险,let 安全;
函数优先,引用共享。”
❤️ 写在最后
JavaScript 的魅力,不仅在于它能做什么,更在于它为什么这么做。
理解 V8 引擎的底层机制,你就能从“写代码的人”蜕变为“掌控代码的人”。
如果你觉得这篇文章帮你打通了任督二脉,欢迎 点赞 ❤️ + 收藏 📌 + 转发!
关注我,带你用工程师思维,看透前端本质。
✨ 延伸阅读:
- 《深入浅出 V8 引擎》
- 《JavaScript 高级程序设计(第4版)》第4章