🔥 从内存到执行JS的执行机制:彻底搞懂 JS 变量、作用域与引擎底层机制

4 阅读5分钟

🔥 从内存到执行:彻底搞懂 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 扫描函数体,按优先级处理声明:

  1. 函数声明 function a() {} → 提升,a = function a() {}
  2. var a → 提升,但不覆盖函数声明(函数优先级更高)
  3. var b → 提升为 undefined

⚠️ 注意:虽然 var a 和函数同名,但函数声明优先于变量声明

此时 fn 的变量环境为:

{
  a: function a() {},
  b: undefined
}

📌 执行阶段

  1. console.log(a) → 输出 function a() {}
  2. var a = 2赋值a 从函数变为数字 2
  3. var b = ab = 2
  4. console.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: 1819 } │
├───────┤         └──────────────────┘
│ obj2 ─┼─────────┘
└───────┘

💡 记住
“赋值对象 = 复制钥匙,不是复制房子”


🆚 六、var vs let:不只是作用域的区别

特性varlet
作用域函数作用域块级作用域
提升行为提升 + 初始化为 undefined提升 + 暂时性死区(TDZ)
重复声明允许禁止
全局属性挂载到 window不挂载

🌰 TDZ 示例:

console.log(x); // ReferenceError!
let x = 10;

var

console.log(y); // undefined(危险!)
var y = 20;

最佳实践
永远用 const / let,彻底告别 var


🚀 七、性能与优化启示

理解这些机制,能帮你写出更高效的代码:

  1. 避免不必要的对象共享
    → 使用深拷贝(structuredClonelodash.cloneDeep

  2. 减少全局变量
    → 全局变量长期驻留内存,不易 GC

  3. 利用块级作用域
    let/const 在块结束后可被回收

  4. 函数声明优于函数表达式
    → 提升更彻底,可提前调用


✅ 八、总结:一张图掌握 JS 执行全貌

[ 代码 ][ 编译阶段 ] → 变量提升 / 函数提升 / 创建执行上下文
   ↓
[ 执行阶段 ] → 赋值 / 调用 / 修改
   ↓
[ 内存模型 ]
   ├─ 原始类型 → 栈(值拷贝)
   └─ 引用类型 → 堆(引用拷贝)
   ↓
[ 调用栈管理 ] → 入栈 → 执行 → 出栈 → GC

🌟 终极口诀
“编译提升,执行赋值;
栈存简单,堆存复杂;
var 危险,let 安全;
函数优先,引用共享。”


❤️ 写在最后

JavaScript 的魅力,不仅在于它能做什么,更在于它为什么这么做
理解 V8 引擎的底层机制,你就能从“写代码的人”蜕变为“掌控代码的人”。

如果你觉得这篇文章帮你打通了任督二脉,欢迎 点赞 ❤️ + 收藏 📌 + 转发
关注我,带你用工程师思维,看透前端本质。

延伸阅读

  • 《深入浅出 V8 引擎》
  • 《JavaScript 高级程序设计(第4版)》第4章