文章

作用域类型与作用域链深度解析

作用域类型与作用域链深度解析

一句话概括

作用域决定了哪些变量在哪个范围内可以访问。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)

letconst{} 内部拥有独立作用域。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 完整差异

这是面试必考题,记住这张表就行:

 varletconst
作用域函数作用域块级作用域块级作用域
可重复声明
可重新赋值
声明提升✅(初始化为 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 enumnamespace —— 底层都是作用域管着的
  • 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。引擎知道变量存在,但禁止在声明前访问。这比 varundefined 更安全——至少不会出现”访问未赋值的变量却不报错”的隐蔽 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 暴露得更早。面试记住三个词:三种作用域 + 差异表 + 从内到外。

本文由作者按照 CC BY 4.0 进行授权