JavaScript闭包与类(Class)的区别及适用场景对比

作者:半夏

时间:2026-11-05

浏览:280

深入对比 JavaScript 闭包与 ES6 类(Class)的底层机制、内存模型、数据私有性及代码复用方式,并提供清晰的技术选型依据与实践注意事项。

文章目录
JavaScript闭包与类(Class)的区别及适用场景对比
在 JavaScript 中,闭包(Closure)与类(Class)是实现数据封装与状态保持的两种核心机制。虽然它们都能对外暴露操作接口并对内隐藏实现细节,但两者的底层依托截然不同:闭包依赖函数作用域链和词法环境(Lexical Environment),而类依托原型链(Prototype Chain)与构造函数。选择哪种机制直接决定了代码的内存开销、扩展能力、私有性保障以及维护成本。本文从底层内存模型、实例化机制、核心差异与业务场景四个维度展开对比,帮助开发者在实际项目中做出合理的技术选型。

一、概念本质与封装方式对比

为了管理带状态的业务逻辑,最常见的诉求是:保存内部状态数据,同时提供控制读写的方法。闭包与类用完全不同的范式完成了这件事。

闭包的本质是函数与其周围词法环境的引用的结合。当外层工厂函数执行时,它声明内部状态变量,并返回包含若干方法的普通对象。由于返回的内部函数持续引用外层作用域中的变量,即便外层函数已经执行完毕,该词法环境仍无法被垃圾回收,从而实现私有变量的长期维护。

类的本质是基于原型的面向对象语法糖(ES6+ Class)。类通过 constructor 构造函数初始化挂载在实例自身的字段,而类体内声明的普通方法则统一挂载在类的原型对象(prototype)上。在 ES2022 之后,通过前缀 # 可定义真正的私有字段,由引擎底层私有名称槽(Private Names)实现隔离。

通过实现一个具备存取款与查询余额功能的电子钱包,可以直观对比两者的写法差异:

1. 闭包工厂函数实现方式

function createWallet(initialBalance = 0) {
  // 私有变量:位于工厂函数的词法环境中,外部无法直接读取或修改
  let balance = initialBalance;

  return {
    deposit(amount) {
      if (amount > 0) balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > 0 && balance >= amount) balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const walletA = createWallet(100);
walletA.deposit(50);
console.log(walletA.getBalance()); // 150
console.log(walletA.balance);       // undefined(外部完全无法直接访问)

2. ES6+ 类实现方式

class Wallet {
  // ES2022 私有字段,外部无法通过常规反射读取
  #balance;

  constructor(initialBalance = 0) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount > 0) this.#balance += amount;
    return this.#balance;
  }

  withdraw(amount) {
    if (amount > 0 && this.#balance >= amount) this.#balance -= amount;
    return this.#balance;
  }

  getBalance() {
    return this.#balance;
  }
}

const walletB = new Wallet(100);
walletB.deposit(50);
console.log(walletB.getBalance()); // 150
// console.log(walletB.#balance);  // 语法错误:Private field '#balance' must be declared

二、内存组织与底层结构差异

闭包与类的代码表现虽然相似,但在 JavaScript 引擎的堆内存(Heap)中,两者所占用的结构截然不同。这是决定二者性能和适用场景的物理根源。

在闭包工厂模式中,每次调用 createWallet(),引擎都会在堆中新分配一个 LexicalEnvironment 对象用于保存局部变量。同时,返回的对象中包含的 deposit、withdraw、getBalance 都是在函数执行期间新创建的函数对象。每个函数对象内部都保存着一个隐式的 [[Environment]] 指针指向当次调用的词法环境。如果创建 10,000 个闭包实例,堆中就会存在 10,000 个独立的词法环境以及 30,000 个独立的函数对象实例。

而在类的模式中,方法 deposit、withdraw、getBalance 在脚本解析类声明时,就已经被创建并直接挂载在 Wallet.prototype 上,且仅有一份。当执行 new Wallet() 时,引擎在堆中创建的具体实例对象内部只存储自有字段(如私有槽 #balance)以及指向 Wallet.prototype 的原型指针([[Prototype]])。即便创建 10,000 个实例,它们共同引用的依然是同一个原型对象上的同一套函数指针。

JavaScript闭包与类(Class)的区别及适用场景对比
图 1:闭包多次实例化产生的独立函数副本与类基于原型链的内存共享对比

三、实例化与运行推导流程

理解二者的执行机制有助于我们在调试与性能调优时准确定位问题。JavaScript 引擎在执行闭包工厂与类构造时的内部步骤存在本质差异。

JavaScript闭包与类(Class)的区别及适用场景对比
图 2:闭包工厂函数执行流与 Class new 实例化的运行推导步骤

闭包工厂调用推导链:

  1. 入栈与环境分配:调用 createWallet(),引擎为当前调用分配独立的执行上下文并推入调用栈,同时在堆中开辟词法环境记录(Environment Record)。
  2. 私有状态绑定:执行形参绑定与 let balance 初始化,变量值存储在当前词法记录中。
  3. 闭包方法实例化:依次执行字面量对象中方法的声明,引擎将当前词法环境的指针写入各方法的内部插槽 [[Environment]] 中。
  4. 出栈与保活:返回公开对象,执行上下文从调用栈弹出。由于返回的对象方法仍然持有对该词法环境的强引用,垃圾回收器的可达性分析算法判定该词法环境存活,保留在堆中。

Class 实例化推导链:

  1. 实例对象开辟:遇到 new Wallet() 运算符,引擎立即在堆内存中分配一个全新的空普通对象 {}。
  2. 原型链接建立:将该空对象的内置插槽 [[Prototype]](在浏览器中常反映为 __proto__)指向构造器的 Wallet.prototype。
  3. 上下文绑定与构造执行:将新对象绑定为当前执行环境的 this,随后执行 constructor() 内部逻辑,完成属性赋值以及私有字段 #balance 在引擎内部私有槽中的挂载。
  4. 隐式返回实例:构造函数执行完毕后,隐式返回初始化完成的 this 引用,完成面向对象的实例产出。

四、核心维度的关键区别

在实际工程开发中,开发者需要在私有性、内存占用、继承扩展和调试支持四个关键维度进行权衡。

JavaScript闭包与类(Class)的区别及适用场景对比
图 3:闭包与类在四大核心维度的技术特性对比

1. 数据私有性与安全性

闭包提供了硬隔离(Hard Privacy)。局部变量完全处于外部执行上下文的作用域链之外,任何外部 JavaScript 反射 API(如 Object.keys()、Object.getOwnPropertyNames()、Reflect.ownKeys())都绝对无法读取或列举到闭包内的局部变量。如果模块内需要保存敏感的认证 Token、加解密私钥或不可变内部计数,闭包能提供无可辩驳的安全壁垒。

类的成员传统上全为公有(Public)。虽然开发者常用下划线 _balance 约定为私有,但外部仍可自由读取与修改。ES2022 引入的 #field 私有字段由引擎层面提供词法作用域级别的强制限制,在类外部调用 wallet.#balance 会在静态解析阶段直接报错。不过,私有字段无法动态计算键名(不能 this[#prop]),且在子类中不能直接通过 super 访问父类的私有字段。

2. 内存开销与执行效率

在实例规模较小时,闭包与类的性能差异在现代 V8 等 JIT 引擎优化下几乎可以忽略。但在高频、大规模实例化场景中,两者的差距会被显著放大:

  • 闭包:每个实例都会带来一整套全新的函数对象与独立的词法环境,内存随实例数量线性激增。
  • 类:方法集中在原型对象上,无论实例化多少个对象,原型方法始终只占一份内存,GC 压力小得多。

3. 代码扩展与复用范式

类拥抱标准的继承(Inheritance)体系。通过 extends 关键字,可以轻松实现子类化、方法重写(Override)以及通过 super 调用父类逻辑,能够天然与 UML 图、面向对象设计模式(如工厂、策略、模板方法)以及 TypeScript 类型系统(抽象类、接口契约)无缝协作。

闭包偏向函数组合(Composition)。闭包并不鼓励深层继承链,而是通过高阶函数(Higher-Order Function)、管道(Pipe)或对象混入(Mixins)把细粒度功能组合进目标对象。这种范式完全避免了面向对象中常见的脆弱基类问题和过度设计。

4. 序列化与调试便利性

使用闭包封装的对象在调用 JSON.stringify(wallet) 时,只会序列化暴露在对象键上的公有属性。如果数据全保存在局部闭包中,序列化结果将是一个空对象 {};在浏览器控制台 DevTools 中打印该对象,也只能看到三个方法,必须进入断点并展开 Scope 面板才能观察 Closure 变量。

类实例则不同。构造函数挂载在 this 上的公有属性在控制台直接清晰展开,JSON.stringify 能直接还原其数据结构。

五、适用场景分析与技术选型

没有绝对优越的语法,只有与场景契合的架构选型。根据具体的项目属性和性能指标,推荐按以下决策树进行取舍:

JavaScript闭包与类(Class)的区别及适用场景对比
图 4:业务架构与代码实现选型决策树

1. 优先选择闭包的典型场景

  • 高阶函数与功能增强:防抖(Debounce)、节流(Throttle)、函数柯里化(Curry)、缓存记忆函数(Memoize)。这些场景需要保持上一次执行的计时器 ID 或缓存映射,闭包是最自然、简洁的载体。
  • 单例服务或无继承需求的轻量模块:如轻量级 SDK 工具类、事件总线单例、Redux/Zustand 风格的状态管理 store。此时无需通过 new 实例化,更不涉及 this 丢失问题,闭包工厂能暴露极其纯粹的 API。
  • 敏感数据的安全黑盒封装:如临时授权凭据管理模块,要求业务层任何代码无法通过属性探针读取内部密文。
  • 全面使用 React Hooks 的前端组件库:Hooks(如 useState、useEffect)本质上就是依托闭包实现的状态维持机制。

2. 优先选择类的典型场景

  • 海量同构对象创建:例如 2D/3D 游戏开发中的精灵对象(Sprite)、几何粒子系统、高性能数据表格中的行单元格模型、编译器/解析器中的 AST 语法树节点。原型方法的共享机制能节省数百兆运行内存。
  • 复杂领域模型设计(DDD / OOP):业务系统核心实体,如电商订单、支付流转单、具有明确父子派生关系的权限策略体系,依托 extends 与多态重写能保持领域结构的清晰整洁。
  • 深度结合 TypeScript 的大型工程:类不仅能作为运行时值,还能直接充当类型定义,完美支持泛型约束、抽象方法、装饰器(Decorators)以及依赖注入(DI)容器框架(如 Nest.js、InversifyJS)。

六、工程实践中的避坑指南

陷阱 1:Class 中使用箭头函数字段导致原型共享失效
许多开发者为了避免 React 回调或事件监听中的 this 绑定问题,喜欢在类中使用类字段箭头函数:
class Component { handleClick = () => { ... } }
后果:箭头函数并不会挂载到 Component.prototype 上,而是在 constructor 阶段为每一个实例独立创建一份函数属性。这直接摧毁了类原本具有的原型内存共享优势,使其内存特征退化为与闭包完全相同。如果包含数千个组件实例,应使用原型方法结合在必要处单独绑定。
陷阱 2:闭包引用未清理引发隐蔽内存泄漏
由于闭包持有着外层词法环境的强引用,只要内层函数对象仍被全局对象、DOM 事件监听器或定时器引用,外层作用域中定义的所有变量(即便某些变量在内层函数中未被直接使用)都可能被整块保留在堆中。
对策:在页面卸载或组件销毁时,及时调用 removeEventListener 或清除 setInterval,并显式将引用置为 null,允许垃圾回收器回收整个词法环境。

七、总结

对比维度 闭包 (Closure / Factory) 类 (ES6+ Class)
语法与范式 函数式编程,对象字面量,词法作用域 面向对象编程 (OOP),class 关键字,原型链
私有变量机制 函数局部变量,原生绝对私有 默认公开;ES2022+ 采用 # 私有字段
内存占用 随实例线性增长(每个实例一套函数对象) 集约高效(所有实例共享原型方法)
复用与扩展 函数组合、高阶函数、Mixins 原型链继承、extends、super
this 关键字困扰 无 this 绑定烦恼,API 简单纯粹 需关注 this 上下文丢失与绑定问题
典型最佳应用 防抖/节流、中间件、Hooks、安全数据隔离 高频大量实体、游戏引擎、TypeScript 架构、复杂领域模型

闭包与类并非互斥对立的替代关系,而是 JavaScript 多范式语言特性的互补工具。在开发中,根据数据保密需求、实例创建频次与架构风格合理选用:以闭包驾驭灵活轻量的状态管线,以类构建稳固集约的实体体系。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章