作用域类型与作用域链深度解析
一句话概括
作用域决定了哪些变量在哪个范围内可以访问。JavaScript 有三种作用域——全局、函数、块级(let/const),变量查找时沿着作用域链从内向外逐层搜索。理解作用域是理解闭包、this、模块化的前提。
1
2
3
4
5
6
7
8
9
10
11
12
const global = "全局可见";
function outer() {
const fnScoped = "outer 内部可见";
if (true) {
const blockScoped = "只在 if 块内可见";
console.log(global, fnScoped, blockScoped); // 三层都能访问
}
console.log(blockScoped); // ❌ ReferenceError —— 出了块就没了
}
三种作用域
全局作用域
在任何函数或块之外声明的变量,整个程序都能访问。
1
2
3
4
5
const API_URL = "https://api.example.com"; // 全局
function fetchData() {
console.log(API_URL); // ✅ 可以访问
}
函数作用域
在函数内部用 var 声明的变量,只在函数内可见。let/const 同样受限于函数作用域,但它们更细——还受限于块。
1
2
3
4
5
6
function demo() {
var x = 1;
let y = 2;
console.log(x, y); // ✅
}
console.log(x); // ❌ ReferenceError
块级作用域(ES6)
let 和 const 在 {} 内部拥有独立作用域。var 没有。
1
2
3
4
5
6
7
{
let a = 1;
const b = 2;
var c = 3;
}
console.log(c); // 3 —— var 逃逸了
console.log(a); // ❌ ReferenceError
var / let / const 完整差异
这是面试必考题,记住这张表就行:
| var | let | const | |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 可重复声明 | ✅ | ❌ | ❌ |
| 可重新赋值 | ✅ | ✅ | ❌ |
| 声明提升 | ✅(初始化为 undefined) | ✅(但进入 TDZ) | ✅(但进入 TDZ) |
| 挂到 window | ✅ | ❌ | ❌ |
声明提升与 TDZ:
1
2
3
4
5
6
7
8
console.log(a); // undefined —— var 提升了且初始化为 undefined
var a = 1;
console.log(b); // ❌ ReferenceError —— let 提升了但处于 TDZ,禁止访问
let b = 2;
// TDZ 就是"声明已存在,但在赋值前禁止触碰"的状态
// 从块/函数开头到 let b = 2 这一行之间的区域就是 b 的 TDZ
作用域链:从内到外找变量
当引擎看到一个变量,按这条链搜索:
1
当前作用域 → 外层作用域 → 更外层 → ... → 全局作用域 → 没找到 → ReferenceError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const a = "全局";
function outer() {
const a = "outer";
const b = "只在 outer";
function inner() {
const a = "inner"; // 覆盖了外层的 a
console.log(a); // "inner" —— 就近原则
console.log(b); // "只在 outer" —— inner 没有,去 outer 找
}
inner();
}
outer();
关键点:作用域链的方向永远是从内向外,不会反过来。内层可以访问外层,外层访问不到内层。
其实你每天都在用
- 你写
if (data) { const result = transform(data); }—— 这就是块级作用域,result出了{}就消失,防止变量污染 - ESLint 报
no-var警告 —— 因为var无视块作用域,容易让变量”逃逸” - Vue 组件的
<script setup>里直接声明变量 —— 整个<script setup>就是个模块作用域 - TypeScript 的
const enum、namespace—— 底层都是作用域管着的 for (let i = 0; ...)里的i只在循环体内可见
经典面试题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 说出这段代码的输出和原因
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 4 4 4
// var 是函数作用域,没有块级概念,循环结束后 i = 4
// 三个回调都引用同一个 i,执行时 i 已经是 4 了
// fix:用 let
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log(j), 0);
}
// 1 2 3
// let 每次迭代创建独立的块级绑定
这个问题闭包篇也有,因为 var+for 恰好同时考了作用域和闭包两个点。从作用域角度回答重点是 var 没有块级作用域;从闭包角度回答重点是 捕获的是引用而非值。
常见误解
❌ 误区一:let/const 没有声明提升
有。只是它们不会初始化为 undefined,而是进入 TDZ。引擎知道变量存在,但禁止在声明前访问。这比 var 的 undefined 更安全——至少不会出现”访问未赋值的变量却不报错”的隐蔽 bug。
❌ 误区二:const 声明的对象内容不能改
const 锁定的是绑定(不能重新赋值),不是值。对象和数组的内部可以自由修改:
1
2
3
const obj = { a: 1 };
obj.a = 2; // ✅ 改内容可以
obj = {}; // ❌ 改绑定不行
❌ 误区三:所有 {} 都创建块级作用域
只有用了 let/const 时才会。空的 {} 或者只用 var 的块不会产生新作用域。函数体总是创建函数作用域,这与是否用 let 无关。
❌ 误区四:作用域链只在函数调用时才创建
作用域(词法环境)在代码编写阶段就确定了,函数对象创建时通过 [[Environment]] 绑定了当时的作用域。执行时才沿着这条链查找,但链的结构是定义时定的。
一句话总结
作用域 = 变量的可见范围。
var管到函数,let/const管到块。作用域链从内向外找变量,let/const有 TDZ 让 bug 暴露得更早。面试记住三个词:三种作用域 + 差异表 + 从内到外。