一、ES5 只有“函数国土”——3 个反直觉现场
先把时间拨回 2015 年之前,ES5 里根本没有块级作用域,var 声明 + 变量提升 把代码变成“玄学现场”。
1. for 循环泄露:闭包全是 3
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () { console.log(i); };
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3
底层原理
- V8 编译阶段把
var i提到全局 variableEnvironment; - 三次循环只是对同一块内存写值;
- 函数 [[Scope]] 指向同一环境 → 执行时统一拿到最终值。
2. if 块“漂”到外面
function foo(flag) {
if (flag) { var tmp = 'hello'; }
console.log(tmp); // undefined 而不是 ReferenceError
}
foo(false);
底层原理
var tmp 在函数作用域内直接提升,块 {} 只是“视觉围栏”,不产生新环境。
3. 参数被变量声明覆盖
function bar(x) {
console.log(x); // function x {}
var x = 5;
console.log(x); // 5
}
bar(10);
底层原理
形参 → 变量提升 → 函数声明 三股力量在 variableEnvironment 里“抢名字”,函数权重最高,于是第一次 log 看到函数体。
二、JS 执行机制速览
-
V8 引擎两阶段
- 编译:扫描声明,创建执行上下文(变量环境 + 词法环境)
- 执行:逐行取值,遇到函数就压调用栈,完毕弹出回收内存
-
调用栈(以函数为单位)
| show() | ← 栈顶 | global main | ← 栈底 -
执行上下文内部
ExecutionContext ├─ variableEnvironment ← var / function 声明 ├─ lexicalEnvironment ← let / const / class └─ outerEnvironmentReference
三、变量提升——“快上车”的设计缺陷
-
为什么当初要提升?
- 10 天工期,KPI 项目,浏览器大战,越简单越好
- 没有块级作用域,直接把变量统统抬到函数顶部,编译器实现最省事
-
带来的坑
- 不知不觉覆盖全局
- 本应销毁的变量幸存下来
- 代码阅读跳来跳去,调试全靠 console
四、ES6「一国两制」——新区旧区分开治理
TC39 不能 break 互联网,于是设计“双轨制”:
| 居民 | 养老地点 | 制度 | 是否提升 |
|---|---|---|---|
| var / function | variableEnvironment(旧区) | 函数作用域 | ✅ 完全提升 |
| let / const / class | lexicalEnvironment(新区) | 块级作用域 | ❌ 仅注册,不提升 |
引擎内部执行上下文示意图:
ExecutionContext
├─ variableEnvironment ← var 挂这里
├─ lexicalEnvironment ← let/const 挂这里(块级栈)
└─ outerEnv
五、新区也有红线:暂时性死区(TDZ)
来看一段“必挂”代码:
function show() {
let a = 1;
{
console.log(a); // ReferenceError
let a;
a = 2;
}
}
show();
拆解
- 进入内部块,V8 发现
let a→ 新建 lexicalEnvironment 栈帧,注册a为“未初始化”; - 在
let a之前的任何访问都会触发 TDZ; - 即使外层
a = 1已经存在,也被“同名遮蔽”但尚未初始化,于是报错。
一句话记忆
“块里重新 let,先写后用是王道;提前读取必爆炸,TDZ 教你做人。”
六、V8 底层栈图:块级作用域如何“用完即焚”
{
let x = 10;
console.log(x);
}
console.log(x); // ReferenceError
运行时 lexicalEnvironment 维护一个小型栈:
[global] → [funcEnv] → [blockEnv: x=10] // 块内
[global] → [funcEnv] // 块结束,弹出 → x 销毁
块执行完,栈帧弹出,变量内存被回收,真正实现“所见即所得”的局部寿命。
七、写给日常开发的 3 条国策
- 默认
const→ 实在需要再改let,var写进史书; - 把
{}当国境线,绝不把 let/const 变量 leak 出去; - 调试打开 DevTools Scope 面板,一眼分辨 variableEnvironment vs lexicalEnvironment。
八、30 秒口诀背完走人
var 养老置顶头,
let/const 块内留;
同名再 let 先别用,
TDZ 等你炸街口。
点赞、收藏、转发,评论区聊聊你踩过最惨的作用域坑!