JavaScript 闭包与作用域简明指南

Frontend2026-09-19TryQuickToolBox

你正在编写一个返回另一个函数的 JavaScript 函数。内部函数使用了外部函数中的变量,但当你稍后调用它时,外部函数已经执行完毕。然而,该变量仍然存在。这是如何做到的?这就是闭包与作用域的魔力。许多开发者对这个概念感到困惑,导致诸如意外的变量值或内存泄漏等错误。在本文中,我们将通过实际示例用简单的术语解析 JavaScript 闭包与作用域。

JavaScript 中的作用域是什么?

作用域决定了变量在代码中的可访问位置。JavaScript 主要有三种作用域类型:

这里有一个快速示例:

let globalVar = 'I am global';

function myFunc() {
  let functionVar = 'I am in function scope';
  if (true) {
    let blockVar = 'I am in block scope';
    console.log(globalVar); // accessible
    console.log(functionVar); // accessible
    console.log(blockVar); // accessible
  }
  console.log(blockVar); // ReferenceError: blockVar is not defined
}

myFunc();
console.log(functionVar); // ReferenceError: functionVar is not defined

理解作用域至关重要,因为它可以防止变量冲突并帮助管理内存。

词法作用域:闭包的基础

JavaScript 使用词法作用域,这意味着变量的作用域由其源代码中的位置决定。内部函数可以访问其外部(封闭)作用域中的变量。这也称为静态作用域。

考虑以下代码:

function outer() {
  let outerVar = 'outer';
  function inner() {
    console.log(outerVar); // 'outer'
  }
  inner();
}
outer();

inner 函数可以访问 outerVar,因为它在词法上位于 outer 内部。这不是动态的;它不依赖于函数的调用方式。

什么是闭包?

闭包是一个函数,即使在该函数在其词法作用域之外执行时,它仍然记得其词法作用域。换句话说,闭包让你可以从内部函数访问外部函数的作用域。闭包在每次创建函数时创建,在函数创建时。

示例:

function makeCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

这里,makeCounter 返回一个内部函数,该函数递增 count。即使 makeCounter 执行完毕,返回的函数仍然可以访问 count。这就是闭包。

闭包底层工作原理

当创建一个函数时,它会存储对其词法环境(作用域链)的引用。当函数被调用时,它使用该环境来解析变量。只要闭包存在,该环境就会保持存活。

闭包的实际用途

闭包不仅仅是理论上的;它们在 JavaScript 中无处不在。

1. 数据隐私(模块模式)

在 ES6 模块之前,闭包被用来创建私有变量:

const counterModule = (function() {
  let count = 0;
  return {
    increment: function() { count++; },
    getCount: function() { return count; }
  };
})();

counterModule.increment();
console.log(counterModule.getCount()); // 1
console.log(counterModule.count); // undefined

这里,count 是私有的,只能通过返回的方法访问。

2. 事件处理程序和回调

闭包通常用于事件处理程序中,以访问外部作用域的变量:

function setupButton() {
  let clickCount = 0;
  document.getElementById('myButton').addEventListener('click', function() {
    clickCount++;
    console.log(`Button clicked ${clickCount} times`);
  });
}

事件处理程序闭包记住了 clickCount。

3. 函数工厂

你可以创建带有预设参数的函数:

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

const double = multiplyBy(2);
console.log(double(5)); // 10

常见陷阱及如何避免

如果不理解闭包,可能会导致意外行为。

循环变量捕获

在循环中使用 var 会创建一个单一共享绑定:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 3, 3, 3
  }, 100);
}

所有回调共享同一个 i。要解决此问题,请使用 let(块作用域)或 IIFE:

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // prints 0, 1, 2
  }, 100);
}

内存泄漏

闭包使变量保持存活。如果闭包附加到长寿命对象(如 DOM 元素),它可能会阻止垃圾回收。当不再需要时,始终移除事件监听器。

作用域链和变量解析

当你访问一个变量时,JavaScript 会查找作用域链:首先是当前作用域,然后是外部作用域,直到全局作用域。如果未找到,则抛出 ReferenceError。

示例:

let a = 'global';
function outer() {
  let b = 'outer';
  function inner() {
    let c = 'inner';
    console.log(a, b, c); // all accessible
  }
  inner();
}
outer();

inner 的作用域链是:inner -> outer -> global。

比较:var、let 和 const

关键字作用域变量提升可重新赋值
var函数是(初始化为 undefined)是
let块是(但未初始化,TDZ)是
const块是(但未初始化,TDZ)否

理解这些差异是避免作用域相关错误的关键。

常见问题

JavaScript 中的闭包是什么?

闭包是一个函数,即使在其词法作用域之外执行,它仍然保留对该作用域的访问。它允许函数“记住”其定义位置的变量。

为什么闭包很重要?

闭包支持数据隐私、函数工厂和保持状态的事件处理程序。它们是许多 JavaScript 模式的基础。

如何避免闭包相关的错误?

使用 let 和 const 进行块作用域,注意循环,并清理事件监听器以防止内存泄漏。

准备好测试你的理解了吗?试试我们的 JSON Formatter 来试验代码片段,并实时查看闭包如何工作。