🚀 揭秘JS执行机制:从“代码乱跑”到“按部就班”的逆袭之路

80 阅读6分钟

🚀 揭秘JS执行机制:从“代码乱跑”到“按部就班”的逆袭之路

你有没有遇到过这种场景👇

showName(); // 居然能执行??
console.log(myName); // 不报错??

var myName = '张三';
function showName() {
  console.log('我居然被提前调用了!');
}

💡 输出结果:

我居然被提前调用了!
undefined

这完全违背了“先声明再使用”的常识啊!难道 JS 在抽风?

❌ 错了!不是 JS 抽风,而是它有一套你不知道的 “潜规则”

今天我们就来揭开 V8 引擎背后的秘密,让你彻底明白:

🔍 为什么函数能“先调后定”?
🔍 varlet 到底差在哪?
🔍 变量提升是魔法还是机制?
🔍 调用栈到底是个啥?

准备好了吗?发车咯——


🤔 一、反常识代码大揭秘:为什么能“未声明先访问”?

我们先来看一段让人怀疑人生的代码:

showName(); 
console.log(myName);

var myName = '张三';
function showName() {
  console.log('我被执行了!');
}

你以为它会报错?但实际运行正常!

✅ 真相只有一个:JS 先编译,再执行

很多人以为 JS 是“边读边执行”的脚本语言。
但其实:V8 引擎在执行前,偷偷做了一波“预处理”

就像餐馆做饭:

  • 第一步:备菜(编译阶段
  • 第二步:炒菜(执行阶段

🔍 二、V8 的“双步舞”:编译 vs 执行

🎯 编译阶段:引擎的“剧透时间”

V8 会快速扫描代码,完成三件事:

动作示例
1. 找 var 声明var a; → 提升,值为 undefined
2. 找函数声明function fn(){} → 完整提升
3. 找 let/const 声明let b; → 提升但不可访问(TDZ)

📌 这个过程叫 创建执行上下文,相当于给变量和函数“安排座位”。

🎯 执行阶段:演员正式登场

编译完成后,JS 才开始逐行执行代码,比如赋值、调用函数等。


🪑 教室比喻法:不同声明的“座位待遇”

我们可以把内存空间想象成一个教室,不同的声明方式坐的位置不一样:

graph LR
A[教室] --> B["VIP区: 函数声明"]
A --> C["普通区: var 变量"]
A --> D["隔离区: let/const (TDZ)"]
区域特点
VIP区(函数声明)提前入座,自带名牌,随时可调用
普通区(var)提前入座,但名字牌写着“暂无”(undefined
隔离区(let/const)已登记,但禁止接触,直到正式声明

⚠️ 注意:let/const 虽然也被“发现”,但在声明前访问会直接报错 —— 这就是 暂时性死区(TDZ)


🚜 三、变量提升:不是魔法,是机制!

🌰 var 的“宽松政策”

console.log(a); // undefined ← 已登记,值为 undefined
var a = 1;
console.log(a); // 1
var a = 2; // 不报错,相当于重新赋值

👉 var 就像一个允许重复布置的房间,谁最后布置谁说了算。

🌰 let 的“严格管控”

console.log(b); // ❌ 报错!Cannot access 'b' before initialization
let b = 3;

// let b = 4; // ❌ 直接报错:Identifier 'b' has already been declared

👉 let 更像是“实名制房间”,不允许抢占,也不允许重复入住。

最佳实践建议
新项目一律使用 let/const,告别隐蔽 bug!


🏆 四、函数提升:比变量更“嚣张”

函数声明的优先级最高,甚至能覆盖同名变量!

fn(); // 输出:"我是函数声明"

var fn = function() {
  console.log("我是函数表达式");
};

function fn() {
  console.log("我是函数声明");
}

🤔 为什么会这样?

因为编译阶段的处理顺序是:

  1. 先处理函数声明 → fn = function 声明体
  2. 再处理 var fn → 发现已存在,跳过
  3. 最后执行赋值语句 → fn = 函数表达式(但函数早已被调用)

结论:函数声明 > 函数表达式 > 变量赋值


⚠️ 箭头函数不会被提升!

func(); // ❌ 报错!
let func = () => {
  console.log('我是箭头函数');
};

因为箭头函数本质是 函数表达式,遵循 let 规则,不能提前使用。


🧠 五、执行上下文:代码运行的“专属舞台”

每次执行代码时,V8 都会创建一个 执行上下文对象,就像为演员搭建专属舞台。

{
  VariableEnvironment: { /* var 和 function */ },
  LexicalEnvironment:   { /* let/const */ },
  ThisBinding:           // this 指向
}
  • 全局代码 → 创建 全局执行上下文
  • 函数调用 → 创建 函数执行上下文

这些上下文会被推入 调用栈(Call Stack) 中管理。


🗃️ 六、调用栈:函数执行的“排队系统”

你可以把它理解为银行叫号机:

graph TB
A["[全局上下文]"] --> B["first() 调用"]
B --> C["[全局] ← [first]"]
C --> D["second() 调用"]
D --> E["[全局] ← [first] ← [second]"]
E --> F["second 执行完 → 出栈"]
F --> G["回到 first 继续"]
G --> H["first 执行完 → 出栈"]
H --> I["回到全局"]

示例代码

console.log('全局开始');

function first() {
  console.log('first函数');
  second();
}

function second() {
  console.log('second函数');
}

first();
console.log('全局结束');

📌 输出顺序

全局开始
first函数
second函数
全局结束

✅ 特点:先进后出,栈顶执行,执行完即销毁,自动垃圾回收。


🔑 七、简单类型 vs 复杂类型:赋值的本质差异

很多新手混淆“赋值”的行为,其实是没搞清数据存储方式。

📦 简单类型(栈内存):复制“内容”

let str = 'hello';
let str2 = str; // 相当于复印文件
str2 = '你好';
console.log(str); // 'hello',不受影响

📁 复杂类型(堆内存):复制“地址”

let obj = { name: '郑老板', age: 18 };
let obj2 = obj; // 相当于分享文件路径
obj.age++;
console.log(obj2.age); // 19!跟着变了

📌 总结一句话:

简单类型赋值 = 复印文件
复杂类型赋值 = 分享链接


🛡️ 八、实战避坑指南(建议收藏⭐)

✅ 1. 少用 var,多用 let/const

// ❌ 危险写法
var a = 1;
var a = 2; // 不报错,但容易埋雷

// ✅ 推荐写法
let a = 1;
// let a = 2; // 直接报错,早发现问题

✅ 2. 函数声明 vs 函数表达式

使用场景推荐方式
需要提前调用function fn() {}
事件监听、模块导出const handleClick = () => {}

✅ 3. 警惕暂时性死区(TDZ)

// ❌ 错误写法
console.log(username); // 报错!
let username = '张三';

// ✅ 正确写法
let username;
console.log(username); // undefined,安全
username = '张三';

🎯 九、终极总结:JS执行的“潜规则手册”

规则说明
✅ 先编译,再执行所有提升都发生在编译阶段
✅ 函数声明最优先能覆盖同名变量
var 松散,let 严格推荐使用 let/const
✅ TDZ 保护机制防止提前访问 let/const
✅ 调用栈管理执行顺序先进后出,函数执行完即销毁
✅ 数据类型决定赋值方式栈存值,堆存地址

💬 写给正在学习的你

“理解执行机制,就像拿到了 JS 的源代码权限。”

当你不再被“变量提升”困扰,当你能预测哪段代码会报错、哪段能运行,你就已经超越了大多数初学者。


🏁 结语:从“被代码耍”到“耍代码玩”

JS 的执行机制看似复杂,其实就三句话:

  1. 先编译,后执行 —— 提前占位是关键
  2. 函数最牛,var 次之,let 最严 —— 提升有优先级
  3. 查变量走作用域链,赋值要看数据类型 —— 存储方式决定行为

掌握这些,你就能真正“掌控”代码,而不是被它牵着鼻子走。


👏 如果这篇文章帮你理清了思路,请务必点赞 ❤️ + 收藏 ⭐ + 分享 🔄!
📢 欢迎在评论区留下你的疑问或心得,我们一起讨论,共同进步!