文章

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

深入解析全局作用域、函数作用域、块级作用域与变量查找规则

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

一句话概括

作用域决定了变量的可见性和生命周期——理解作用域链,就是理解 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;
}

变量查找顺序

  1. 当前作用域 → 2. 父级作用域 → 3. 祖父级作用域 → … → 全局作用域
  2. 找到则停止,找不到则报错 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 的区别是什么?

参考答案:

特性varlet/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 按以下顺序查找:

  1. 当前作用域:在当前函数的 EnvironmentRecord 中查找
  2. 父级作用域:通过 outer 引用向上查找
  3. 递归向上:直到找到变量或到达全局作用域
  4. 找不到:抛出 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

总结与扩展

核心要点

  1. 三种作用域:全局、函数、块级(let/const)
  2. 作用域链:从内到外的变量查找机制
  3. 静态性:作用域在函数定义时确定
  4. var vs let:函数作用域 vs 块级作用域

延伸学习方向

  • 执行上下文:理解代码运行时的环境管理
  • 闭包:作用域链 + 闭包 = 闭包能够”穿越时空”
  • this 绑定:this 的指向与作用域的关系
  • 模块化:利用函数作用域实现私有变量

相关主题

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