🚀 揭秘JS执行机制:从“代码乱跑”到“按部就班”的逆袭之路
你有没有遇到过这种场景👇
showName(); // 居然能执行??
console.log(myName); // 不报错??
var myName = '张三';
function showName() {
console.log('我居然被提前调用了!');
}
💡 输出结果:
我居然被提前调用了!
undefined
这完全违背了“先声明再使用”的常识啊!难道 JS 在抽风?
❌ 错了!不是 JS 抽风,而是它有一套你不知道的 “潜规则”。
今天我们就来揭开 V8 引擎背后的秘密,让你彻底明白:
🔍 为什么函数能“先调后定”?
🔍var和let到底差在哪?
🔍 变量提升是魔法还是机制?
🔍 调用栈到底是个啥?
准备好了吗?发车咯——
🤔 一、反常识代码大揭秘:为什么能“未声明先访问”?
我们先来看一段让人怀疑人生的代码:
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("我是函数声明");
}
🤔 为什么会这样?
因为编译阶段的处理顺序是:
- 先处理函数声明 →
fn = function 声明体 - 再处理
var fn→ 发现已存在,跳过 - 最后执行赋值语句 →
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 的执行机制看似复杂,其实就三句话:
- 先编译,后执行 —— 提前占位是关键
- 函数最牛,var 次之,let 最严 —— 提升有优先级
- 查变量走作用域链,赋值要看数据类型 —— 存储方式决定行为
掌握这些,你就能真正“掌控”代码,而不是被它牵着鼻子走。
👏 如果这篇文章帮你理清了思路,请务必点赞 ❤️ + 收藏 ⭐ + 分享 🔄!
📢 欢迎在评论区留下你的疑问或心得,我们一起讨论,共同进步!