一句话总结:
JS 的作用域,看似玄学,实则能用“宫斗剧 + 行李托运 + 机房机架”讲清楚。
如果你过去对 JS 的作用域、执行上下文、变量提升、块级作用域、暂时性死区感到疑惑,那你一定要看完这篇文章。保证你看完之后,看到 var/let/const 能像看三兄弟一样:这是混世魔王老大(var)、这是规规矩矩老二(let)、这是乖到不行的小妹(const)。
🏰 第一章:作用域到底是什么?看不见的“隐形围栏”
什么是作用域?
作用域 = 一种变量的“活动范围” + “领地意识”。
在什么地方声明,就能在什么地方“横着走”。
在 JS 世界中,有三种领地:
- 全局作用域:整个地球都能访问,我就是王!
- 函数作用域:我在我自己的小公寓里可以自由活动。
- 块级作用域(ES6 引入) :我在我自己的卧室才安全。
如果你是从别的语言转过来的,例如 Java、Python,你会说:
——“块级作用域不是常识吗?这很正常啊。”
JS: “我以前没这个功能的啦……”
这就不得不谈一谈Javascript的黑历史。
⏳ 第二章:ES5 年代的 JS:一个仓促上线的 KPI 项目
JS 的诞生,用官方话讲:
10 天造出来的语言。
那 10 天里能干嘛?
写出:
- 语法
- 基础类型
- 函数
- 原型
- 执行机制
你让我设计块级作用域?抱歉,我连宿舍都还没找好。
所以 ES5 时代 JS 是这样的:
- 没有块级作用域
- 变量全部“提升”到作用域顶端
- var 乱飞
- 函数和变量同名还能互相覆盖
于是你会看到各种经典迷惑行为:
console.log(a); // undefined
var a = 10;
你以为这是 JS 在耍帅?
实际上这是:
v8 在编译阶段帮你偷偷把 var 挪到作用域顶部了。
这也是为什么大家说:
👉变量提升是 JS 的设计缺陷之一。
👉但为了兼容历史包袱,只能一路背到今天。
🧠 第三章:执行上下文:作用域背后的“大总管”
想理解 JS 的作用域,我们必须引出一个关键角色:
执行上下文(Execution Context)
这是 JS 执行任何代码时必须创建的一个包装盒。
每个上下文里有两个重要领地:
| 名称 | 存放内容 | 专属行为 |
|---|---|---|
| 变量环境(Variable Environment) | 存 var 声明 | 会进行变量提升 |
| 词法环境(Lexical Environment) | 存 let/const + 块级作用域 | 存在暂时性死区、不可提升 |
一个用户看不到的 内部结构 大概像这样:
执行上下文
│
├── 变量环境 (var)
│ └── a = undefined(先创建)
│
└── 词法环境 (let/const)
└── 并不是 undefined,而是 TDZ(暂时性死区)
这就解释了为什么这段代码会报错:
console.log(b); // ❌ ReferenceError
let b = 10;
因为 b 在词法环境的 TDZ(暂时性死区) 中
“你声明我之前,我都不会让你碰!”
🧱 第四章:块级作用域是怎么被“硬加进去”的?
ES6 必须兼容之前 ES5 的逻辑,又要支持现代语言都有的块级作用域。
怎么办?
V8 的解决方案非常优雅:用栈结构模拟每个块级作用域。
当执行到一个 {}:
- 给词法环境 push 一个新的“块层”
- 在这一层中放
let/const - 当块结束,词法环境被 pop 掉
于是代码:
let a = 1
{
let a = 2
console.log(a)
}
console.log(a)
在词法环境中大概长这样:
词法环境 (stack)
│
├── 全局层: a = 1
│
└── 块级层: a = 2(优先查这里)
输出:
2
1
块级作用域执行完,内部的 a 就从栈里 pop 出去了,外部永远访问不到。
漂亮!
现代化语言的能力就这样被“硬塞”进 JS 里。
🔥 第五章:ES5 vs ES6 —— 一国两制的诞生
为了兼容从 1995 年一路传下来的奇葩设计,JS 有了这样有趣的现象:
👉 var 属于“变量环境”
- 会提升
- 全局污染
- 无块级作用域
- 函数作用域
👉 let/const 属于“词法环境”
- 不提升
- 有暂时性死区
- 有块级作用域
- 每个
{}都独立
这就是所谓的:
JS 执行上下文里的“一国两制”:
一个作用域,两个环境(VE + LE)。
也是为什么你会看到这种鬼畜差异:
var 的经典鬼故事
if (true) {
var name = '大厂苗子'
}
console.log(name) // 输出:大厂苗子
let 的健康逻辑
if (true) {
let name = '大厂苗子'
}
console.log(name) // ❌ ReferenceError
同样的代码,行为却完全不同。
原因你已经知道了:它们不在一个“环境”里。
🎭 第六章:作用域链:变量查找的路线图
当 JS 查找一个变量时,它会:
- 先查当前的作用域层(词法环境/变量环境)
- 如果没有,向外层找
- 一直找到全局
- 如果还没有,报错
这就是 作用域链(Scope Chain)
可以简单地理解为:
我 ↓
我爸 ↓
我爷爷 ↓
太爷 ↓
……
如果所有祖宗都没有这个变量,那就:
ReferenceError: xxx is not defined
💣 第七章:为什么大家说变量提升是设计缺陷?
因为它会导致各种迷惑和 bug:
❌ 1. 看起来变量还没声明,但已经拿来用了
console.log(a) // undefined
var a = 10
初学者一脸问号:这语言还能透视未来吗?
❌ 2. var 会污染全局 scope
for (var i = 0; i < 5; i++) {}
console.log(i) // 5
你只是跑了个循环,它却给全局塞了一个变量。
❌ 3. 不同块级里的 var 会互相覆盖
if (true) var a = 1;
if (true) var a = 2;
console.log(a) // 2
写着写着:
“我是谁?我在哪?我变量怎么变了?”
🚀 第八章:现代 JS 是怎么让 var 和 let 和谐共存的?
全靠执行上下文的“二元结构”:
✔ var 放在变量环境(VE)
- 会提升
- 无块级作用域
✔ let/const 放在词法环境(LE)
- 有块级作用域
- 有暂时性死区
- 不提升
- 用栈结构维护嵌套块级作用域
换句话说:
JS 并没有抛弃 var,而是把它关在“老区”,
让 let/const 住进“新区”。
一国两制,各自安好。
🎯 第九章:你应该如何记住这些概念?
给你一个最通俗比喻:
var —— 住在“公共宿舍”的老大哥
- 进去就占床位(提升)
- 谁都能看到它(无块级作用域)
- 还喜欢串寝(覆盖变量)
let —— 住单人间的好青年
- 进房前不能访问(TDZ)
- 房间说关门就关门(块级作用域)
- 不允许和别人重名(不重复声明)
const —— 把门焊死的租客
- 一旦入住不能换房间(不能重新赋值)
🧩 最终总结:5 张图带你彻底理解作用域
1. JS 执行代码前——创建执行上下文
执行上下文
│
├── 变量环境(var 提升)
└── 词法环境(let/const,存在 TDZ)
2. 遇到块级作用域 {}
词法环境.push(新的块层)
3. 块完成
词法环境.pop()
4. 查找变量
当前层 → 外层…… → 全局
5. var 依然保留,但被限制在变量环境里
let/const 才是新时代的主流
🎉 写在最后:JS 底层并不难,只是没人给你讲“正确的故事”
作用域通常被很多人讲得:
- 抽象
- 抽象
- 还是抽象
但其实:
只要把执行上下文想象成房子、想象成栈、想象成小区
所有概念都会瞬间清晰。
如果你准备继续深入 JS 底层(比如原型链、闭包、事件循环、宏任务微任务),随时叫我,我可以继续给你写“掘金风格·底层通俗系列”