菜单

模块模式

相关源文件

目的与范围

本文档解释了 JavaScript 模块模式,这是一种代码组织技术,它利用词法作用域和闭包来创建具有受控可见性的封装代码单元。模块模式允许开发人员将相关数据和功能分组,同时隐藏私有实现细节并选择性地公开公共 API。

本文档特别关注模块模式作为闭包概念的应用。有关闭包本身的信息,请参阅 闭包,有关限制作用域暴露的一般信息,请参阅 限制作用域暴露

来源: scope-closures/ch8.md1-12 scope-closures/ch1.md16-23

封装和最小化暴露 (POLE)

封装是指将服务于共同目的的数据和行为捆绑在一起,同时控制某些方面的可见性。最小化暴露原则 (POLE) 旨在防御性地防止变量和函数作用域过度暴露的危险。

模块模式结合了这两个原则,提供了若干好处

  1. 更好的代码组织 — 通过建立清晰的边界,使软件的构建和维护更加容易
  2. 提高代码质量 — 避免了暴露过多数据和功能的陷阱
  3. 受控访问 — 区分私有实现细节和公共 API

一个设计良好的模块会隐藏其内部细节,并通过明确定义的接口仅公开必要的内容。

来源: scope-closures/ch8.md14-28

什么构成一个模块

要理解什么构成了真正的模块,将其与其他具有相似性但又不完全是模块的模式进行比较会很有帮助

命名空间(无状态分组)

命名空间只是一个在通用对象下分组的相关函数的集合,没有数据或状态

// NOT a module - just a namespace
var Utils = {
    cancelEvt(evt) { /* ... */ },
    wait(ms) { /* ... */ },
    isValidEmail(email) { /* ... */ }
};

虽然它组织了相关功能,但它不满足模块的状态化和封装方面。

数据结构(有状态分组)

数据结构包含数据和操作该数据的功能,但隐藏了任何实现细节

// NOT a module - just a data structure
var Student = {
    records: [ /* student data */ ],
    getName(studentID) { /* ... */ }
};

尽管它结合了数据和功能,但它缺乏定义真正模块的可见性控制。

模块(有状态访问控制)

真正的模块结合了数据、功能和访问控制。关键特征是:

  1. 一个外部作用域(通常来自工厂函数或 IIFE)
  2. 在该作用域内维护的私有隐藏状态
  3. 一个公共 API,至少包含一个对隐藏状态具有闭包的函数

来源: scope-closures/ch8.md30-200 scope-closures/ch1.md19-22

经典模块模式

经典模块模式(也称为“暴露模块”模式)通常使用 IIFE 来创建一个隐藏的作用域,并返回一个充当公共 API 的对象。

单例模块 (IIFE)

单例模块是使用 IIFE 创建的,该 IIFE 执行一次并返回公共 API

var Student = (function defineStudent(){
    // Private implementation
    var records = [ /* student data */ ];
    
    function getName(studentID) {
        /* implementation */
    }
    
    // Public API
    var publicAPI = {
        getName: getName
    };
    
    return publicAPI;
})();

// Usage
Student.getName(73); // "Suzy"

IIFE 创建了一个包含私有 records 数组的作用域。 getName 函数对此作用域具有闭包,允许它在 IIFE 完成后访问私有数据。只有明确添加到返回的 publicAPI 对象的内容才能被外部世界访问。

模块工厂(多个实例)

当需要模块的多个实例时,该模式可以调整为使用工厂函数

function defineStudent() {
    // Private implementation
    var records = [ /* student data */ ];
    
    function getName(studentID) {
        /* implementation */
    }
    
    // Public API
    var publicAPI = {
        getName: getName
    };
    
    return publicAPI;
}

// Create instances
var fullTime = defineStudent();
fullTime.getName(73); // "Suzy"

每个实例都有自己的私有作用域和状态,公共方法具有对该特定实例私有数据的闭包。

来源: scope-closures/ch8.md99-199 scope-closures/ch1.md19-22

现代模块系统

随着 JavaScript 的发展,出现了更多标准化的模块系统来解决经典模式的局限性。

Node CommonJS 模块

Node.js 引入了 CommonJS 模块格式,它是基于文件的(每个文件一个模块)

// Private implementation
var records = [ /* student data */ ];

function getName(studentID) {
    /* implementation */
}

// Public API
module.exports.getName = getName;

CommonJS 模块的关键特征

  1. 文件中的所有内容默认都是私有的
  2. module.exports 对象定义了公共 API
  3. 模块使用 require() 加载
  4. 模块是单例实例
// Consuming a CommonJS module
var Student = require("/path/to/student.js");
Student.getName(73); // "Suzy"

// Or import specific parts
var { getName } = require("/path/to/student.js");

来源: scope-closures/ch8.md202-281 scope-closures/ch4.md302-370

ES 模块 (ESM)

ES 模块是在 ES6 中引入的标准模块系统

// Private implementation
var records = [ /* student data */ ];

function getName(studentID) {
    /* implementation */
}

// Public API
export { getName };

ES 模块的关键特征

  1. 像 CommonJS 一样基于文件
  2. 所有内容默认都是私有的
  3. 模块文件自动处于严格模式
  4. export 关键字公开公共 API 成员
  5. import 关键字加载模块
  6. 模块是单例实例

ESM 提供了多种导出和导入的语法变体

导出变体

  • 命名导出: export { getName }
  • 内联导出: export function getName() { /* ... */ }
  • 默认导出: export default function getName() { /* ... */ }

导入变体

  • 命名导入: import { getName } from "./student.js"
  • 默认导入: import getName from "./student.js"
  • 命名空间导入: import * as Student from "./student.js"
  • 重命名导入: import { getName as getStudentName } from "./student.js"

来源: scope-closures/ch8.md282-381 scope-closures/ch4.md278-301

模块和闭包

促成模块模式的核心机制是闭包。模块的公共 API 中的函数即使在模块定义执行完成后,也能保持对模块私有变量的访问。

模块和闭包之间的这种关系解释了为什么理解词法作用域对于掌握模块模式至关重要

  1. 模块创建一个词法作用域(通过 IIFE、工厂函数或文件作用域)
  2. 私有状态定义在此作用域内
  3. 公共 API 函数在同一作用域中定义,并可以访问私有状态
  4. 即使在模块定义完成后,公共 API 函数也通过闭包保持对私有状态的访问
  5. 这使得模块可以在函数调用之间维护状态

来源: scope-closures/ch8.md382-393 scope-closures/ch1.md19-22

模块模式最佳实践

在实现模块模式时,请考虑以下最佳实践

实践描述
遵循 POLE默认将所有内容设为私有,仅公开必要的内容
清晰的 API为您的模块定义清晰、最小化的接口
避免全局泄漏不要用模块内部信息污染全局作用域
一致的风格在一个项目中保持一致的模块模式
偏好标准格式尽可能使用 CommonJS 或 ESM 而非自定义实现
分组相关功能将相关数据和函数保留在同一模块中
管理依赖项清晰地定义和导入依赖项

有关更复杂的模块组织模式和变体细节,请参阅附录 A 中的 经典模块变体

来源: scope-closures/ch8.md382-393 scope-closures/ch8.md29-28

总结

模块模式是 JavaScript 编程中最重要的代码组织模式之一。通过结合词法作用域和闭包,模块提供了强大的封装和信息隐藏机制。

需要记住的关键概念

  1. 一个模块需要

    • 一个外部作用域(函数或文件)
    • 该作用域内的私有状态
    • 公共 API 中的至少一个函数具有对私有状态的闭包
  2. 现代 JavaScript 提供了多种实现模块的方式

    • 经典模块模式(IIFE 或工厂)
    • CommonJS 模块(Node.js)
    • ES 模块 (ESM)
  3. 无论采用哪种实现方法,所有模块都利用相同的词法作用域和闭包基本原理来维护状态和控制访问。

模块模式代表了作用域和闭包概念的实际汇合,它提供了一种结构化的代码组织方法,强调最小化暴露原则(POLE)。

来源: scope-closures/ch8.md382-393 scope-closures/ch1.md16-23