首页/文章/八股文

JavaScript 闭包详解

2025-10-09
14075 分钟
...

JavaScript 闭包(Closure)详解

一、什么是闭包

**闭包(Closure)**是 JavaScript 中一个非常核心且常被问到的概念。简单来说:

闭包是一个函数,它可以“记住”并访问其定义时所在的词法作用域,即使这个函数在其作用域之外被调用。

换句话说,当一个函数“嵌套”在另一个函数中,并且内部函数引用了外部函数的变量时,就形成了闭包。

示例:

function outer() {
  const name = 'hua';
  function inner() {
    console.log(name); // 可以访问外层函数的变量
  }
  return inner;
}

const fn = outer();
fn(); // 输出 "hua"

虽然 outer() 已经执行完毕,但 inner() 依然可以访问 outer() 中的 name。这就是闭包。


二、闭包的形成条件

要形成闭包,必须满足三个条件:

  1. 函数嵌套:存在一个函数嵌套在另一个函数内部;
  2. 外部函数变量被内部函数引用
  3. 外部函数被执行并返回内部函数,从而让内部函数在外部作用域中仍然可访问。
function makeCounter() {
  let count = 0;
  return function() {
    count++;
    console.log(count);
  };
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3

makeCounter() 执行后返回了一个匿名函数,而这个匿名函数“记住”了 makeCounter() 的变量 count,即使 makeCounter() 已经结束。


三、闭包的原理(词法作用域 + 垃圾回收机制)

理解闭包的本质,离不开两个关键点:

1. 词法作用域(Lexical Scope)

JavaScript 的作用域在函数定义时确定,而不是在执行时确定。
这意味着内部函数天然就可以“捕获”外层函数的变量。

2. 垃圾回收机制(GC)

当外部函数执行完后,正常情况下其局部变量会被销毁。
但如果这些变量仍被内部函数引用,则它们会被“保留”在内存中,这样就形成了闭包。


四、闭包的常见应用场景

1. 封装私有变量

闭包可以用来模拟私有变量,避免外部直接访问:

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
      console.log(`存入 ${amount},当前余额:${balance}`);
    },
    withdraw(amount) {
      if (amount <= balance) {
        balance -= amount;
        console.log(`取出 ${amount},当前余额:${balance}`);
      } else {
        console.log('余额不足');
      }
    }
  };
}

const account = createBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.balance); // undefined,无法直接访问

2. 函数柯里化(Currying)

闭包是柯里化实现的核心:

function add(a) {
  return function(b) {
    return a + b;
  };
}

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

add(1) 返回的函数记住了 a=1,这就是闭包在起作用。


3. 在循环中保存变量(经典面试题)

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出: 3, 3, 3

原因是 var 没有块级作用域,所有回调共享同一个 i

利用闭包可以解决:

for (var i = 0; i < 3; i++) {
  (function(i) {
    setTimeout(() => console.log(i), 100);
  })(i);
}
// 输出: 0, 1, 2

或者使用 let(块级作用域)也能解决。


五、闭包的优点与缺点

✅ 优点

  1. 数据私有化:防止外部直接修改内部状态;
  2. 延长变量生命周期:可在函数外使用函数内的数据;
  3. 模块化开发:通过闭包封装逻辑,形成独立模块。

⚠️ 缺点

  1. 可能导致内存泄漏:闭包会持有变量引用,若使用不当,会使内存无法被释放;
  2. 调试困难:因为变量作用域链较长;
  3. 滥用闭包可能影响性能:尤其在循环中频繁创建闭包时。

六、闭包与内存泄漏问题

闭包会让一些本该被销毁的变量常驻内存。例如:

function test() {
  let data = new Array(100000).fill('some data');
  return () => console.log(data[0]);
}

const leak = test(); // data 未被释放

解决方案:

  • 不再需要闭包时手动解除引用:
leak = null;
  • 避免在全局环境长期保存闭包引用;
  • 只在必要时使用闭包。

七、闭包与模块化模式

闭包可以帮助我们模拟模块系统:

const CounterModule = (function() {
  let count = 0;

  function add() {
    count++;
    console.log(count);
  }

  function reset() {
    count = 0;
  }

  return {
    add,
    reset
  };
})();

CounterModule.add(); // 1
CounterModule.add(); // 2
CounterModule.reset();
CounterModule.add(); // 1

这里的 count 相当于私有变量,只能通过暴露的函数访问。


八、闭包的底层实现机制(作用域链)

每个函数都有一个内部属性 [[Environment]],它指向定义该函数时的词法环境(Lexical Environment)。
当函数被调用时,会创建一个新的执行上下文,其中的作用域链包含:

  1. 当前函数的变量对象;
  2. 外部函数的变量对象;
  3. 全局对象。

当函数访问变量时,JS 引擎会沿着这条作用域链逐层查找。


九、总结

特性说明
定义闭包是函数与其外部词法环境的组合
本质函数可以访问其定义时作用域中的变量
优点数据私有化、延长生命周期、模块化封装
缺点容易造成内存泄漏、调试困难
应用场景计数器、私有变量、函数柯里化、模块化开发

十、思维导图

闭包Closure
 ├─ 概念函数 + 外部环境
 ├─ 条件函数嵌套 + 外部变量引用 + 外部函数执行
 ├─ 原理词法作用域 + GC机制
 ├─ 应用
 │   ├─ 私有变量
 │   ├─ 柯里化
 │   ├─ 循环变量捕获
 │   ├─ 模块化开发
 ├─ 优缺点
 │   ├─ 优点封装延长生命周期
 │   ├─ 缺点内存泄漏性能风险
 └─ 注意事项释放引用合理使用

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2025-10-13
cookie跨域介绍
Cookie 跨域问题详解 在 Web 开发中, Cookie 是最常见的客户端存储机制之一,用于记录用户登录状态、偏好设置、会话信息等。然而,当涉及到 跨域请求 时,Cookie 的行为往往变得复杂,尤其在现代浏览器的安全策略下,跨域 Cookie 的传递和写入都有严格的限制。 本文将从基础概念开始,逐步讲解 Cookie 的跨域机制、SameSite 属性、CORS 配置及常见问题解决方案。 一、Cookie 的基础概念 Cooki...
学习面试
八股文2025-10-07
开始性能优化之旅
事件循环机制 一、JavaScript 引擎的本质 核心职责 : 解析 JavaScript 语法 管理变量和内存 执行代码逻辑 不涉及 : 线程管理(Worker除外) I/O 操作 定时器控制 网络请求 常见引擎:V8(Chrome)、SpiderMonkey(Firefox)、JavaScriptCore(Safari) 二、宿主环境的扩展能力 宿主提供的多线程能力 : | 线程类型 | 功能 | 对应 API | | | | |...
学习面试
八股文2025-09-30
前端首屏优化
话说,我在面试的时候,80%的情况下,都会被问到首屏优化问题,烦,恨 TCP Slow Start(慢启动)概念 咱先不说标题的数字哪里来的,先说一个概念,就是TCP的慢启动。 你们想哈,在浏览器和服务器开始建立连接的时候,服务器是并不知道浏览端网络的带宽、拥塞状况。假设你本地的带宽是1M,如果一开始服务器就发送2M的文件,那浏览器压根就不是人类,忍忍或者挤挤就能接收到的,这样就可能会引发丢包和重传。 想想,这个该有什么办法解决这个问题...
学习面试
八股文2025-09-28
Function.prototype和Object.prototype
一道面试题引发的原型链血案,彻底搞懂 Function.prototype 和 Object.prototype 前言 有这样一道题: 目录 问题分析与答案揭晓 让我们先公布正确答案,然后深入分析原因: 为什么 是 而不是 ? 这是因为函数和实例的原型链路径完全不同! 原型链的本质机制 JavaScript中的两条原型链 在JavaScript中,存在两条不同的原型链: &nbsp; &nbsp; &nbsp; 关键理解:F.proto...
学习面试
八股文2025-09-10
让你彻底明白什么是闭包
今天我们来聊一个听起来很高大上,但实际上你可能天天在用(只是不知道它名字)的概念—— 闭包 。 一、一个你肯定写过的闭包 先别管定义,来看这段代码,你是不是再熟悉不过了? 恭喜你!这就是一个经典的闭包!是不是很简单? 二、为什么会有闭包?—— 背包的故事 想象一下,JavaScript 中的函数就像一个小机器人,当它被创建时,会背着一个神奇的 背包 。 这个背包里装着什么呢?装着它 出生时 所在环境的所有变量! 当我们调用 时,返回的那...
学习面试
八股文2025-09-01
JavaScript 原型链深度解析
JavaScript 原型链深度解析:从概念到实践 前言 JavaScript 原型链是前端开发中最重要也是最容易混淆的概念之一。理解原型链不仅有助于我们掌握 JavaScript 的面向对象编程,更是深入理解继承、方法查找等核心机制的关键。本文将从基础概念开始,逐步深入到原型链的实际应用。 1\. 核心概念理解 什么是原型? 在 JavaScript 中,每个对象都有一个内部属性指向另一个对象,这个被指向的对象就是原型。原型本身也是一...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录