从“变量提升”到“调用栈爆炸”:V8 引擎是如何偷偷执行你的 JavaScript 的?

🚀 从"变量提升"到"调用栈爆炸":V8 引擎是如何偷偷执行你的 JavaScript 的?


你有没有写过这样的代码,然后一脸懵逼地看着控制台输出 undefined

ini 复制代码
console.log(myName); // undefined?
var myName = 'chen';

或者更离谱的:

scss 复制代码
showName(); // 居然能执行?!
function showName() {
    console.log('函数showName被执行');
}

别慌!这不是魔法,也不是浏览器在跟你开玩笑。这背后,是 V8 引擎 在编译阶段悄悄为你安排的一切。

今天我们就来揭开 JavaScript 执行机制的神秘面纱------让你不仅知道"发生了什么",还知道"为什么发生",顺便还能在面试时装个大杯 ☕️!


🧠 一、JS 不是"边解释边执行"的吗?怎么还有"编译"?

很多人以为 JavaScript 是"纯解释型语言",一行一行地执行,错了就报错。但实际上,现代 JS 引擎(比如 Chrome 的 V8)早就不是这样了!

V8 会把你的代码 先快速编译(JIT 编译) ,再执行。整个过程分为两个阶段:

  • 编译阶段(Compilation Phase)
  • 执行阶段(Execution Phase)

而且这两个阶段几乎是"无缝衔接"的------你感觉不到编译的存在,但它确实在执行前的一刹那完成了所有准备工作。

💡 小知识:JIT = Just-In-Time(即时编译),意思是"用的时候才编译",但其实它比传统解释器快得多!


🎭 二、变量提升?函数优先?真相在这里!

来看一段经典代码:

ini 复制代码
showName();
console.log(myName);
console.log(hero);

var myName = 'chen';
let hero = '123';

function showName() {
    console.log('函数showName被执行');
}

输出结果是:

javascript 复制代码
函数showName被执行
undefined
ReferenceError: Cannot access 'hero' before initialization

Why?

✅ 编译阶段 V8 干了这些事:

  1. 创建全局执行上下文(Global Execution Context)

  2. 扫描所有 var 声明和函数声明

    • var myName → 提升为 myName = undefined
    • function showName() → 整个函数体被提升(函数声明优先级最高!)
  3. let/const 声明

    • 被放入 词法环境(Lexical Environment)
    • 但处于 暂时性死区(Temporal Dead Zone, TDZ) ,访问就报错!

V8 在执行某段代码前,会先进行快速解析,并为即将执行的代码创建一个执行上下文,其中包含变量环境(存放 var 和函数声明)和词法环境(存放 let/const,并管理 TDZ)。

所以:

  • showName() 能执行 → 函数被完整提升 ✅
  • console.log(myName)undefinedvar 提升但未赋值)✅
  • console.log(hero) → 报错 ❌(let 在 TDZ 中)

🤯 记住口诀:函数 > var > let/const

函数声明最牛,var 次之但会"占座",let/const 最守规矩但脾气大!

函数声明这么厉害,有没有一种方法能够限制他呢? 有的兄弟,有的!

函数表达式(无论是用 varlet 还是 const都不会被提升 !只有函数声明才会被完整提升

就像这样!

go 复制代码
func();     // TypeError: func is not a function
let func = () => {
    console.log('函数表达式不会提升');
}

报错不是因为用了 let,而是因为你根本没写"函数声明"。


🧱 三、执行上下文 & 调用栈:JS 的"舞台后台"

想象 JS 执行就像一场话剧:

  • 调用栈(Call Stack) = 舞台
  • 执行上下文(Execution Context) = 演员的剧本 + 道具箱

每次函数调用,V8 就:

  1. 创建一个新的执行上下文(包含变量环境、词法环境、this 等)
  2. 把它压入调用栈顶部
  3. 执行完后,弹出栈,内存回收(垃圾回收器微笑点头)

举个栗子 🌰:

ini 复制代码
var a = 1;
function fn(a) {
    console.log(a);
    
    var a = 2;
    var b = a;
    console.log(a);
}
fn(3);

输出:

复制代码
3
2

编译阶段(fn 内部):

  • 参数 a 被初始化为 3
  • var a 发现重复声明 → 忽略(var 允许重复)
  • 所以第一个 console.log(a)3

执行阶段:

  • a = 2 赋值 → 第二个输出 2

⚠️ 注意:在非严格模式下,函数声明可能会覆盖同名参数(这是 JS 的历史包袱),但这种写法极易引发混乱,强烈建议避免在函数内用函数声明覆盖参数名!

如果我加一个东西呢

ini 复制代码
var a = 1;
function fn(a) {
    console.log(a);
    
    var a = 2;
    function a() {};   //加一个这个结果会是什么呢
    var b = a;
    console.log(a);
}
fn(3);

结果就变成了

csharp 复制代码
[Function: a]
2

让我们来理解一下

第一步:理解函数内部的"作用域初始化"

在进入函数 fn 执行上下文时,JavaScript 引擎会做以下事情(按顺序):

  1. 创建形参(parameters)
  2. 函数声明提升(function declarations are hoisted and initialized)
  3. 变量声明提升(variable declarations are hoisted but not initialized)

但要注意:函数声明的提升优先级高于变量声明和形参!


第二步:详细展开 fn 内部的提升过程

函数定义:

css 复制代码
function fn(a) {
    console.log(a);
    var a = 2;
    function a() {};
    var b = a;
    console.log(a);
}

提升后,等价于:

css 复制代码
function fn(a) {
    // 步骤1:形参 a 被初始化为传入的值 3
    // 步骤2:函数声明 function a() {} 被提升,并且 **覆盖** 形参 a
    // 步骤3:var a; 也被提升,但因为函数声明已经存在,var 的提升不会覆盖函数

    function a() {}   // 函数声明被提升并初始化
    var a;            // 变量声明被提升,但无效果(因为 a 已经是函数)
    var b;

    // 现在开始执行代码:
    console.log(a);   // 此时 a 是函数 function a() {}

    a = 2;            // 这里把 a 重新赋值为数字 2
    b = a;            // b = 2
    console.log(a);   // 输出 2
}

⚠️ 关键点:函数声明不仅被提升,而且在作用域初始化阶段就被赋值(即函数体),而 var 声明只是提升变量名,不赋值。


第三步:为什么形参 a 被覆盖?

当你写 function fn(a),相当于在函数顶部有一个 var a = 3(传入值)。

但是随后遇到 function a() {},这是一个函数声明,它会:

  • 在作用域创建阶段就绑定名称 a 到这个函数;
  • 覆盖掉形参的初始值

所以,在执行第一行 console.log(a) 之前,a 已经是函数了!


对比:如果没有 function a() {}

如果去掉函数声明:

javascript 复制代码
function fn(a) {
    console.log(a); // 3
    var a = 2;
    console.log(a); // 2
}

这时 var a 的提升不会改变 a 的值(因为 var a 和形参 a 是同一个绑定),所以第一次输出是 3


总结

  • 函数声明会被完全提升(包括函数体),并且优先级高于形参和 var 声明。
  • 因此,在 fn 开始执行时,a 被初始化为 function a() {},而不是传入的 3
  • 后续 var a = 2 是对 a 的重新赋值,所以第二次 console.log(a) 输出 2

🔒 四、var vs let:不只是"能不能重复声明"

特性 var let / const
提升 提升到顶部,值为 undefined 存在 TDZ,不能提前访问
作用域 函数作用域 块级作用域 {}
重复声明 允许(静默忽略) 报错!
全局对象绑定 window.myVar 不绑定
ini 复制代码
var a = 1;
var a = 2; // 没事,JS:我习惯了

let b = 3;
let b = 4; // SyntaxError! 别想糊弄我!

🙅‍♂️ let 的态度: "要么一次到位,要么别碰我!"


🧬 五、值类型 vs 引用类型:复印机 vs 地址条

让我们来看个例子

ini 复制代码
let str = 'hello'; 
let str2 = str; 
str2 = '你好';
console.log(str,str2);
console.log(str.length);
let obj = {  
    name: '张三',
    age: 18
}
let obj2 = obj; 
obj2.age++;
console.log(obj2,obj);

结果是

css 复制代码
hello 你好
5
{ name: '张三', age: 19 } { name: '张三', age: 19 }

诶?奇了怪了,为什么都是更改个值, str 没改 obj 却改了呢?

原因就是str 是简单数据类型,obj 是复杂数据类型

  • 简单数据类型(string/number/boolean) :存在 栈内存,直接复制值
  • 复杂数据类型(object/array/function) :存在 堆内存 ,变量存的是 地址指针

💡 想真正"复制对象"?用 structuredClone()... 展开符,或者 JSON.parse(JSON.stringify())(有坑!慎用)
注:实际内存分配比"栈/堆"模型复杂得多(涉及逃逸分析等),但这个模型对理解赋值行为非常有帮助。


🧩 六、严格模式:让 JS 变得"讲规矩"

javascript 复制代码
'use strict';
var a = 1;
var a = 2; // 在非严格模式下没事,但在严格模式?照样没事!

等等?不是说严格模式更严格吗?

没错,但 var 重复声明在 任何模式下都不报错 (这是历史包袱😅)。

真正会被严格模式拦住的是:

  • 给未声明变量赋值
  • this 指向 undefined(而非 window
  • 删除不可删除的属性等

⚠️ 注意:在非严格模式下,函数声明可能会覆盖同名参数(这是 JS 的历史包袱),但这种写法极易引发混乱,强烈建议避免在函数内用函数声明覆盖参数名!


🎯 总结:V8 的执行流程图

csharp 复制代码
你的代码
   ↓
V8 接管 → 编译阶段(一瞬间!)
   ├─ 创建执行上下文(全局 or 函数)
   ├─ 提升:函数 > var > let/const(TDZ警告!)
   ├─ 初始化参数、变量环境、词法环境
   ↓
执行阶段
   ├─ 从上到下执行可执行代码
   ├─ 遇到函数 → 新建上下文 → 压入调用栈
   └─ 函数结束 → 弹出栈 → 内存回收

💬 最后说两句

JavaScript 的执行机制看似玄学,其实逻辑非常清晰。理解"编译先行"和"执行上下文" ,你就超越了 80% 的前端新手。

下次再看到 undefined 或 TDZ 报错,别慌------那是 V8 在温柔地提醒你:"兄弟,顺序乱了,重排一下!"

相关推荐
San302 小时前
深入理解JavaScript执行机制:从变量提升到内存管理
javascript·编程语言·代码规范
用户12039112947262 小时前
深入理解JavaScript执行机制:从变量提升到调用栈全解析
javascript
weixin_438694392 小时前
pnpm 安装依赖后 仍然启动报的问题
开发语言·前端·javascript·经验分享
烟袅3 小时前
深入 V8 引擎:JavaScript 执行机制全解析(从编译到调用栈)
前端·javascript
有点笨的蛋3 小时前
JavaScript 执行机制深度解析:编译、执行上下文、变量提升、TDZ 与内存模型
前端·javascript
_一两风3 小时前
深入理解JavaScript执行机制:从一道经典面试题说起
javascript
阿凡达蘑菇灯3 小时前
langgraph---条件边
开发语言·前端·javascript
San303 小时前
深入理解浏览器渲染流程:从HTML/CSS到像素的奇妙旅程
javascript·css·html
拖拉斯旋风3 小时前
深入理解 JavaScript 执行机制之V8引擎:从编译到执行的完整生命周期
javascript·面试