跳到主要内容
← 返回文章列表
基础知识2026/08/09约 5 分钟读完

词法环境与闭包

词法环境:变量放在哪里、沿哪里查找 闭包:函数记住并访问定义时的外层环境 作用域链:当前环境一路指向外层环境的查找路径

词法环境与闭包
文章目录 21

先给结论:

  • 词法环境解决的是:“变量存在哪里,以及查找变量时去哪里找。”
  • 闭包解决的是:“函数离开定义它的作用域后,为什么还能访问当时的外部变量。”

两者关系是:

函数在创建时会记录它定义位置对应的外层词法环境。函数与这个词法环境的关联,就是闭包的基础。

一、什么是词法作用域

JavaScript 使用词法作用域 Lexical Scope。

“词法”表示变量的作用域由代码书写位置决定,而不是由函数调用位置决定。

const name = 'global';

function outer() {
  const name = 'outer';

  function inner() {
    console.log(name);
  }

  return inner;
}

const fn = outer();

function run() {
  const name = 'run';
  fn();
}

run();

输出:

outer

虽然 fn() 是在 run() 中调用的,但 inner 定义在 outer 中,所以它寻找 name 时,会沿定义位置的外层作用域查找:

inner 作用域
→ outer 作用域
→ 全局作用域

它不会去调用者 run 的作用域中查找。

因此:

变量在哪里查找
取决于函数在哪里定义
不取决于函数在哪里调用

二、什么是词法环境

词法环境是 JavaScript 规范中的一种抽象结构,用来记录当前作用域内的变量,以及如何找到外层作用域。

可以简化理解为:

interface LexicalEnvironment {
  bindings: Record<string, unknown>;
  outer: LexicalEnvironment | null;
}

它主要包括两个部分:

  • 当前环境中的变量绑定;
  • 指向外层词法环境的引用。

例如:

const globalName = 'JavaScript';

function outer() {
  const age = 10;

  function inner() {
    const message = 'hello';
    console.log(message, age, globalName);
  }

  inner();
}

outer();

可以抽象为:

inner 词法环境
├─ message = "hello"
└─ outer → outer 词法环境
             ├─ age = 10
             └─ outer → 全局词法环境
                          ├─ globalName = "JavaScript"
                          └─ outer → null

执行 console.log(message, age, globalName) 时:

  1. 在 inner 环境找到 message;
  2. 当前环境没有 age,沿 outer 找到 outer 环境;
  3. 当前环境没有 globalName,继续找到全局环境;
  4. 如果一直查找到 outer = null 仍未找到,就抛出 ReferenceError。

这种逐层查找关系通常称为作用域链。

三、什么时候创建词法环境

可以先粗略理解为:进入某个作用域时,会创建对应的词法环境。

全局环境

const appName = 'demo';

appName 位于全局词法环境。

函数调用环境

function add(a, b) {
  const result = a + b;
  return result;
}

add(1, 2);
add(3, 4);

每调用一次 add(),都会创建一个独立的函数执行环境:

第一次调用:
a = 1
b = 2
result = 3

第二次调用:
a = 3
b = 4
result = 7

因此,两次调用中的局部变量互不影响。

块级环境

let 和 const 具有块级作用域:

{
  const value = 1;
  let count = 2;
}

console.log(value); // ReferenceError

这个代码块会形成相应的块级词法环境。

var 没有普通块级作用域:

{
  var value = 1;
}

console.log(value); // 1

四、什么是闭包

闭包可以定义为:

函数以及该函数创建时所关联的词法环境。

更通俗地说:

内层函数记住了它定义时的外层变量,即使外层函数已经执行结束,它仍然可以访问这些变量。

经典例子:

function createCounter() {
  let count = 0;

  return function increment() {
    count++;
    return count;
  };
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

第一次执行:

const counter = createCounter();

会发生:

  1. 调用 createCounter();
  2. 创建本次调用的词法环境;
  3. 创建 count,初始值为 0;
  4. 创建 increment 函数;
  5. increment 记录自己定义位置的外层词法环境;
  6. 返回 increment;
  7. counter 保存对 increment 的引用。

引用关系可以表示为:

counter
  ↓
increment 函数
  ↓
createCounter 本次调用的词法环境
  ↓
count = 0

虽然 createCounter() 已经执行完毕,但返回的函数仍然引用它的词法环境。因此该环境仍然可达,count 不会被垃圾回收。

每次执行 counter(),访问的都是同一个 count。

五、为什么两个闭包互不影响

const counterA = createCounter();
const counterB = createCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1

每次调用 createCounter(),都会创建一个新的词法环境:

counterA
  ↓
increment A
  ↓
词法环境 A
  └─ count = 2

counterB
  ↓
increment B
  ↓
词法环境 B
  └─ count = 1

它们不是共享一个 count,而是分别捕获了不同调用产生的词法环境。

所以准确说法是:

两次调用创建了两个独立的词法环境,两个闭包分别引用各自环境中的 count。

六、闭包什么时候可以被回收

let counter = createCounter();

counter();      // 1
counter = null;

赋值之前:

counter → increment → 词法环境 → count

赋值之后:

counter → null

如果没有其他地方引用 increment 函数,那么:

  • increment 变得不可达;
  • 它关联的词法环境变得不可达;
  • count 也可以被垃圾回收。

注意,是“可以被回收”,不代表立即回收。真正的回收时间由 JavaScript 引擎决定。

七、闭包的常见使用场景

1. 保存私有状态

function createWallet(initialMoney) {
  let money = initialMoney;

  return {
    deposit(amount) {
      money += amount;
    },

    getBalance() {
      return money;
    },
  };
}

const wallet = createWallet(100);

wallet.deposit(50);
console.log(wallet.getBalance()); // 150

外部无法直接访问 money,只能通过暴露的方法操作它。

2. 函数柯里化和参数复用

function multiplyBy(factor) {
  return function (value) {
    return factor * value;
  };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

3. 事件处理器

function bindButton(button, message) {
  button.addEventListener('click', () => {
    console.log(message);
  });
}

事件触发时,外层函数可能早已执行完,但回调仍然能够访问 message。

4. 防抖

function debounce<T extends (...args: never[]) => void>(
  fn: T,
  delay: number,
) {
  let timer: ReturnType<typeof setTimeout> | undefined;

  return (...args: Parameters<T>) => {
    if (timer !== undefined) {
      clearTimeout(timer);
    }

    timer = setTimeout(() => {
      fn(...args);
    }, delay);
  };
}

返回函数通过闭包保存 timer,因此多次调用可以操作同一个定时器。

八、闭包可能导致内存问题吗

闭包本身不是内存泄漏。只有不再需要的数据仍然被可达对象引用时,才可能造成内存长期占用。

function createHandler() {
  const largeData = new Array(1_000_000).fill('data');

  return function handler() {
    console.log(largeData.length);
  };
}

const handler = createHandler();
window.addEventListener('click', handler);

只要事件监听器仍然引用 handler,handler 关联的词法环境和 largeData 就可能一直存在。

不再使用时应解除引用:

window.removeEventListener('click', handler);

常见风险来源包括:

  • 未移除的事件监听器;
  • 未清理的定时器;
  • 长期存在的回调;
  • 缓存中保存了不再需要的闭包;
  • 闭包捕获了大型对象。

九、常见误区

误区一:只有返回内部函数才叫闭包

不是。函数只要引用了外层词法环境中的变量,就体现了闭包机制。返回内部函数只是最容易观察的场景。

function outer() {
  const value = 1;

  setTimeout(() => {
    console.log(value);
  }, 1000);
}

这里的定时器回调也形成了闭包。

误区二:闭包会复制外部变量的值

闭包通常保留的是对变量绑定的访问,不是简单复制当时的值:

function outer() {
  let value = 1;

  const getValue = () => value;

  value = 2;

  return getValue;
}

console.log(outer()()); // 2

误区三:闭包中的变量永远不会释放

只有闭包函数仍然可达时,相关环境才需要保留。闭包失去所有引用后,对应数据仍然可以被垃圾回收。

误区四:闭包和作用域是同一个概念

不是:

  • 作用域描述变量在哪些代码区域可访问;
  • 词法环境是规范用于管理变量绑定和外层引用的抽象结构;
  • 闭包是函数与其定义时词法环境的关联。

十、面试标准回答

可以这样回答:

JavaScript 使用词法作用域,变量查找由代码定义位置决定。词法环境用于记录当前作用域中的变量绑定,并通过外层引用形成作用域链。函数创建时会记录其定义位置对应的外层词法环境,因此函数即使在外层函数执行结束后被调用,仍然能够访问外层变量,这种函数与词法环境的关联称为闭包。闭包常用于保存私有状态、事件回调、函数柯里化和防抖,但如果长期存在的闭包捕获了大型对象,也需要注意内存占用。

最简记忆方式:

词法环境:变量放在哪里、沿哪里查找
闭包:函数记住并访问定义时的外层环境
作用域链:当前环境一路指向外层环境的查找路径

评论

0 条

评论会在审核后展示,邮箱不会公开。

正在确认登录状态

5000 / 5000