作用域类型与作用域链深度解析
深入解析全局作用域、函数作用域、块级作用域与变量查找规则
作用域类型与作用域链深度解析
一句话概括
作用域决定了变量的可见性和生命周期——理解作用域链,就是理解 JavaScript 如何从内到外查找变量的过程。
背景
作用域是 JavaScript 的基石概念。所有变量、函数都在特定作用域中运行:
- 写代码时,你声明的变量在哪个范围可用?
- 代码执行时,变量名到底指向哪个变量?
- 闭包为什么能”记住”外层变量?
这些问题都和作用域有关。面试中,作用域是闭包、原型链、this 绑定的必备前置知识。
概念与定义
什么是作用域?
作用域(Scope)是指变量、函数的可访问范围。JavaScript 有三种作用域类型:
| 作用域类型 | 声明方式 | 特点 | 面试高频点 |
|---|---|---|---|
| 全局作用域 | 最外层声明 | 整个脚本/页面可访问 | 变量污染、挂载 window |
| 函数作用域 | function 内部 | 仅函数内部可见 | var 无块级、闭包基础 |
| 块级作用域 | let/const + {} | 仅 {} 内部可见 | for 循环、暂时性死区 |
什么是作用域链?
作用域链(Scope Chain)是嵌套作用域形成的链式结构,用于变量查找:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const global = "全局"; // 全局作用域
function outer() { // outer 函数作用域
const outerVar = "外部";
function inner() { // inner 函数作用域
const innerVar = "内部";
console.log(innerVar); // 1. 先在 inner 作用域找
console.log(outerVar); // 2. 沿着作用域链向上找
console.log(global); // 3. 找到全局作用域
}
return inner;
}
变量查找顺序:
- 当前作用域 → 2. 父级作用域 → 3. 祖父级作用域 → … → 全局作用域
- 找到则停止,找不到则报错
ReferenceError
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 全局变量
const a = "全局";
function test() {
// 函数作用域
const b = "函数";
if (true) {
// 块级作用域(ES6+)
const c = "块级";
console.log(a, b, c); // 都能访问
}
// console.log(c); // ❌ 报错:c is not defined(块级作用域外)
}
console.log(a); // "全局"
// console.log(b); // ❌ 报错:b is not defined(函数作用域外)
核心知识点拆解
1. var vs let vs const 的作用域差异
1
2
3
4
5
6
7
8
9
10
11
12
13
// var:函数作用域(没有块级)
if (true) {
var a = "var";
}
console.log(a); // "var" —— if 块外仍能访问
// let/const:块级作用域
if (true) {
let b = "let";
const c = "const";
}
console.log(b); // ❌ ReferenceError: b is not defined
console.log(c); // ❌ ReferenceError: c is not defined
2. for 循环的作用域问题
1
2
3
4
5
6
7
8
9
// ❌ var 无块级:循环后所有回调共享同一个 i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 3, 3, 3
}
// ✅ let 有块级:每次迭代创建独立的 i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2
}
3. 作用域链的静态性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const x = "全局";
function outer() {
const y = "外部";
return function inner() {
// 闭包形成:inner 出生时记住了 outer 的作用域
console.log(x, y);
};
}
const fn = outer();
// 即使在全局执行,依然能访问 outer 的变量
fn(); // 输出: "全局 外部"
💡 关键点:作用域在函数定义时确定,而非运行时。
实战案例
案例一:循环打印问题(经典面试题)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 错误:所有 setTimeout 共享同一个 i
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), i * 100);
}
// 输出: 5, 5, 5, 5, 5
// ✅ 解决方案1:使用 let
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), i * 100);
}
// 输出: 0, 1, 2, 3, 4
// ✅ 解决方案2:使用 IIFE 捕获每次的 i
for (var i = 0; i < 5; i++) {
((j) => {
setTimeout(() => console.log(j), j * 100);
})(i);
}
// ✅ 解决方案3:使用立即执行函数返回函数
for (var i = 0; i < 5; i++) {
setTimeout(((j) => () => console.log(j))(i), i * 100);
}
案例二:模块化中的私有作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 使用函数作用域实现私有变量
const Module = (() => {
// 私有变量(函数作用域)
let _count = 0;
// 私有方法
function _increment() {
_count++;
}
// 公共接口
return {
getCount: () => _count,
increment: () => {
_increment();
return _count;
}
};
})();
console.log(Module.getCount()); // 0
Module.increment();
console.log(Module.getCount()); // 1
// console.log(_count); // ❌ 报错:_count 未定义
案例三:科里化与作用域链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 科里化:利用闭包保存参数,形成作用域链
function add(a) {
return function (b) {
return a + b; // 访问外层参数 a
};
}
const add5 = add(5); // 闭包记住 a = 5
console.log(add5(3)); // 8
console.log(add5(10)); // 15
// 箭头函数版本
const multiply = (a) => (b) => a * b;
const double = multiply(2);
console.log(double(5)); // 10
底层原理
执行上下文与作用域
JavaScript 执行代码时,会创建执行上下文:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 执行上下文结构
ExecutionContext = {
LexicalEnvironment: { // 词法环境(变量存储)
EnvironmentRecord: { // 环境记录
a: "全局",
outer: <Function>
},
outer: <null> // 外部环境引用
},
VariableEnvironment: { // 变量环境(var 声明)
...
}
}
// 函数调用时创建新上下文
outer() 执行时:
ExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
y: "外部",
inner: <Function>
},
outer: <GlobalContext> // 指向父级上下文
}
}
作用域链的构建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function outer() {
const a = 1;
function inner() {
const b = 2;
console.log(a, b);
}
return inner;
}
const fn = outer();
fn();
// 运行时作用域链:
// inner 的 [[Scope]] -> outer 的 [[Scope]] -> Global
// 实际查找过程:
// 1. 找 b: inner.EnvironmentRecord.b ✓
// 2. 找 a: inner.outer.a ✓ (outer.EnvironmentRecord)
// 3. 找不存在的变量: 遍历到全局仍找不到 -> ReferenceError
💡 人话总结:
- 作用域就是变量的”势力范围”
- 作用域链就是“就近原则”——先看自己家,再看邻居家,再看更远的…
- var 是租房(函数作用域),let/const 是买房(块级作用域)
- 闭包之所以能”穿越时空”,是因为它带着出生时的作用域链
高频面试题解析
Q1:var 和 let/const 的区别是什么?
参考答案:
| 特性 | var | let/const |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 变量提升 | 有提升(值为 undefined) | 有提升(暂时性死区) |
| 重复声明 | 允许 | 不允许 |
| 全局挂载 | 挂载 window | 不挂载 |
| 暂时性死区 | 无 | 有(TDZ) |
1
2
3
4
5
6
7
// var 变量提升
console.log(a); // undefined(不是报错)
var a = 1;
// let 暂时性死区
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 1;
Q2:什么是暂时性死区(TDZ)?
参考答案: let/const 声明的变量在初始化前访问会报错,这段区域称为”暂时性死区”(Temporal Dead Zone)。
1
2
3
4
5
6
7
{
// TDZ 区域
// console.log(x); // ReferenceError
let x = 1; // 此处 TDZ 结束
console.log(x); // 1
}
TDZ 的意义:强制先声明再使用,避免变量的默认值 undefined 带来的隐患。
Q3:作用域链是怎么工作的?
参考答案: 当访问一个变量时,JavaScript 按以下顺序查找:
- 当前作用域:在当前函数的 EnvironmentRecord 中查找
- 父级作用域:通过 outer 引用向上查找
- 递归向上:直到找到变量或到达全局作用域
- 找不到:抛出 ReferenceError
作用域链是静态的——在函数定义时就确定了,与函数在哪里调用无关。
Q4:如何理解”JavaScript 没有块级作用域”这句话?
参考答案: 这是针对 var 而言的。在 ES6 之前,if、for、while 等块 {} 不会创建独立的作用域:
1
2
3
4
5
6
7
8
9
10
if (true) {
var a = 1;
}
console.log(a); // 1 —— 能访问,var 没有块级作用域
// let/const 才有块级作用域
if (true) {
let b = 1;
}
console.log(b); // ReferenceError
总结与扩展
核心要点
- 三种作用域:全局、函数、块级(let/const)
- 作用域链:从内到外的变量查找机制
- 静态性:作用域在函数定义时确定
- var vs let:函数作用域 vs 块级作用域
延伸学习方向
- 执行上下文:理解代码运行时的环境管理
- 闭包:作用域链 + 闭包 = 闭包能够”穿越时空”
- this 绑定:this 的指向与作用域的关系
- 模块化:利用函数作用域实现私有变量
相关主题
本文由作者按照 CC BY 4.0 进行授权