本文档解释了 JavaScript 模块模式,这是一种代码组织技术,它利用词法作用域和闭包来创建具有受控可见性的封装代码单元。模块模式允许开发人员将相关数据和功能分组,同时隐藏私有实现细节并选择性地公开公共 API。
本文档特别关注模块模式作为闭包概念的应用。有关闭包本身的信息,请参阅 闭包,有关限制作用域暴露的一般信息,请参阅 限制作用域暴露。
来源: scope-closures/ch8.md1-12 scope-closures/ch1.md16-23
封装是指将服务于共同目的的数据和行为捆绑在一起,同时控制某些方面的可见性。最小化暴露原则 (POLE) 旨在防御性地防止变量和函数作用域过度暴露的危险。
模块模式结合了这两个原则,提供了若干好处
一个设计良好的模块会隐藏其内部细节,并通过明确定义的接口仅公开必要的内容。
来源: 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) { /* ... */ }
};
尽管它结合了数据和功能,但它缺乏定义真正模块的可见性控制。
真正的模块结合了数据、功能和访问控制。关键特征是:
来源: scope-closures/ch8.md30-200 scope-closures/ch1.md19-22
经典模块模式(也称为“暴露模块”模式)通常使用 IIFE 来创建一个隐藏的作用域,并返回一个充当公共 API 的对象。
单例模块是使用 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.js 引入了 CommonJS 模块格式,它是基于文件的(每个文件一个模块)
// Private implementation
var records = [ /* student data */ ];
function getName(studentID) {
/* implementation */
}
// Public API
module.exports.getName = getName;
CommonJS 模块的关键特征
module.exports 对象定义了公共 APIrequire() 加载// 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 模块是在 ES6 中引入的标准模块系统
// Private implementation
var records = [ /* student data */ ];
function getName(studentID) {
/* implementation */
}
// Public API
export { getName };
ES 模块的关键特征
export 关键字公开公共 API 成员import 关键字加载模块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 中的函数即使在模块定义执行完成后,也能保持对模块私有变量的访问。
模块和闭包之间的这种关系解释了为什么理解词法作用域对于掌握模块模式至关重要
来源: 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 编程中最重要的代码组织模式之一。通过结合词法作用域和闭包,模块提供了强大的封装和信息隐藏机制。
需要记住的关键概念
一个模块需要
现代 JavaScript 提供了多种实现模块的方式
无论采用哪种实现方法,所有模块都利用相同的词法作用域和闭包基本原理来维护状态和控制访问。
模块模式代表了作用域和闭包概念的实际汇合,它提供了一种结构化的代码组织方法,强调最小化暴露原则(POLE)。