彻底搞懂 JavaScript 闭包:原理、陷阱与修复方案

Cora
Cora
发布时间:2026-10-13| 1人浏览过
原创

JavaScript 闭包常让开发者感到困惑,但其本质是“有权访问另一个函数作用域中变量的函数”。本文从定义出发,深入剖析执行环境、作用域链与垃圾回收机制,揭示闭包维持数据的原理。同时,重点解决循环中变量捕获失败的经典缺陷,并提供通过立即执行函数(IIFE)修复的代码方案,助你写出更健壮的 JS 代码。

闭包的定义与核心特征

闭包是指有权访问另外一个函数作用域中的变量的函数。创建闭包的最常见方式,就是在一个函数内部创建另一个函数。

function a() {
  var i = 0;
  function b() {
    alert(++i);
  }
  return b;
}
var c = a();
c(); // 弹出窗口显示 1

这段代码体现了闭包的两大特征:
1. 函数 b 嵌套在函数 a 内部;
2. 函数 a 返回了内部函数 b。
当执行 var c = a() 后,变量 c 指向了函数 b。由于外部变量 c 引用了内部函数 b,而 b 又依赖 a 中的变量 i,因此形成了一条引用链,这就是闭包。

为什么需要闭包?

闭包的主要作用在于保护变量和维持状态。

  • 保护函数内的变量安全:如上例所示,变量 i 只在函数 a 内部可见,外部无法直接访问,只能通过返回的函数 b 间接操作,从而避免了命名冲突和数据被意外修改。
  • 使变量始终保存在内存中:一般函数执行完后,局部变量会被垃圾回收机制(GC)释放。但因为闭包的存在,函数 a 返回后,其内部的 i 依然存在于内存中供 b 使用。这使得每次调用 c() 时,i 都能保留上次的值并自增。

如果 a 不返回 b,则 a 和 b 互相引用但都不被外界引用,它们将被 GC 回收。只有当内部函数被外部引用时,闭包才真正建立,外层函数的资源才会被保留。

深入理解:执行环境与作用域链

要彻底理解闭包,需了解函数执行时的四个关键概念:执行环境、活动对象、作用域和作用域链。

  1. 定义时:当定义函数 a 时,JS 解释器将 a 的作用域链设置为 a 所在的“环境”。若 a 是全局函数,作用域链仅包含 window 对象。
  2. 进入执行环境:当 a 执行时,进入相应的执行环境。
  3. 创建作用域属性:为 a 添加 scope 属性,指向定义时的作用域链。
  4. 创建活动对象:创建一个活动对象(Call Object),并将其添加到作用域链顶端。此时 a 的作用域链包含 a 的活动对象和 window。
  5. 初始化参数与内部函数:在活动对象上添加 arguments 以及形参和内部函数 b 的引用。此时,b 的作用域链被设置为 a 的作用域。

至此,a 完成定义到执行的步骤。返回 b 的引用给 c 后,由于 b 的作用域链包含对 a 活动对象的引用,b 可以访问 a 中的所有变量。因为 b 被 c 引用且依赖 a,所以 a 不会被回收。

变量查找机制:当在 b 中访问变量时,搜索顺序为:b 的活动对象 → a 的活动对象 → window。若存在原型对象,则在查找完自身活动对象后先查找原型对象。

内存管理与垃圾回收

在 JavaScript 中,若对象不再被引用,则被 GC 回收。闭包的代价是内存占用。

正常情况下,函数执行结束后,其执行上下文和作用域链占用的空间会被释放。

function test() {
  var str = "hello world";
  console.log(str);
}
test();

但在闭包场景下,由于内部函数引用了外部函数的变量,外部函数的作用域活动对象虽然会被销毁,但其内容被闭包函数的作用域链所引用,直到闭包函数被销毁后,这些内存才会被释放。滥用闭包会导致大量内存消耗,甚至引起内存泄漏。

经典缺陷:循环中的变量捕获

闭包最常见的副作用是:内部函数只能取到外层函数变量的最终值,而非定义时的快照值。

以下代码尝试通过循环绑定点击事件,期望弹出对应的索引值,但实际结果全是最终值(如数组长度为 4,则全部弹出 4):

/* 闭包缺陷演示 */
(function($) {
  var result = new Array(),
      i = 0;
  for (; i < 10; i++) {
    result[i] = function () {
      return i;
    };
  }
  $.RES1 = result;
})(jQuery);
// 执行数组中的函数,返回结果均为 10
$.RES1[0]();

第 1 步:分析错误原因

在 for 循环中,i 是外层匿名函数的局部变量。循环结束时,i 的值变为 10。数组中的每个函数都引用了这个同一个 i 地址。当任意一个函数被调用时,它读取的是当前 i 的值,即 10。

第 2 步:使用 IIFE 修复缺陷

通过引入立即执行函数表达式(IIFE),为每次循环创建一个独立的作用域,将当前的 i 值作为参数传递给内部函数,从而固定变量值。

/* 修复闭包缺陷 */
(function($) {
  var result = new Array(),
      i = 0;
  for (; i < 10; i++) {
    result[i] = function (num) {
      return function () {
        return num;
      }
    }(i); // 立即执行,传入当前 i 的值
  }
  $.RES2 = result;
})(jQuery);
// 调用闭包函数,正确返回对应索引
console.log($.RES2[0]()); // 输出 0

此修复方案在内存中创建了新的活动对象,使得 num 保存了每次循环时 i 的具体值,从而解决了引用最终值的问题。

实战案例:DOM 事件绑定

假设页面上有多个 <span> 元素,希望点击每个元素时弹出其索引值。

错误写法

直接使用 var 声明循环变量,所有点击事件共享同一个 i,导致点击任何元素都弹出最终的 i 值。

var spans = $("#divTest span");
for (var i = 0; i < spans.length; i++) {
  spans[i].onclick = function () {
    alert(i); // 始终弹出最终值
  }
}

正确写法

使用 IIFE 包裹事件处理逻辑,将当前的 i 值传递给参数 num,确保每个事件监听器拥有独立的变量副本。

var spans2 = $("#divTest2 span");
$(document).ready(function () {
  for (var i = 0; i < spans2.length; i++) {
    (function (num) {
      spans2[i].onclick = function () {
        alert(num); // 弹出正确的索引值
      }
    })(i);
  }
});

内部函数与逃脱机制

内部函数定义在另一个函数中,默认只能在外部函数内调用。若要在外部调用,必须通过以下方式“逃脱”:

  1. 赋值给全局变量:在外部函数中将内部函数赋值给全局变量。
  2. 作为返回值:外部函数直接返回内部函数的引用。
// 方式一:赋值给全局变量
var globalVar;
function outerFn() {
  function innerFn() { /*...*/ }
  globalVar = innerFn;
}
outerFn();
globalVar(); // 可调用
// innerFn(); // ReferenceError

这种逃脱能力意味着只要存在调用内部函数的可能,JS 运行时就必须保留相关内存,直到最后一个引用废弃,GC 才能回收。这也是闭包能够持久化数据的基础。

自由变量与多闭包交互

当内部函数引用了既不是自身局部变量也不是参数的变量时,该变量称为自由变量。外部函数的调用环境称为封闭闭包的环境。

如果一个外部函数返回多个内部函数,这些内部函数共享同一个自由变量实例:

function outerFn() {
  var outerVar = 0;
  function innerFn1() {
    outerVar++;
    document.write("Inner function 1 outerVar = " + outerVar + "<br/>");
  }
  function innerFn2() {
    outerVar += 2;
    document.write("Inner function 2 outerVar = " + outerVar + "<br/>");
  }
  return { fn1: innerFn1, fn2: innerFn2 };
}
var fnRef = outerFn();
fnRef.fn1(); // outerVar = 1
fnRef.fn2(); // outerVar = 3
fnRef.fn1(); // outerVar = 4

在此例中,innerFn1 和 innerFn2 共享 outerVar。重新调用 outerFn 会创建新的封闭环境和新的变量实例,互不影响。这体现了闭包在模拟面向对象私有变量方面的强大能力。

以上就是 JavaScript 闭包的详细内容,更多关于 JavaScript 高级特性与作用域的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。