JavaScript模块
什么是模块?
模块是JavaScript代码的封装单元,允许将代码分割成独立、可复用的部分。每个模块可以包含变量、函数、类等,并且可以控制哪些内容对外暴露(导出),哪些内容只在模块内部使用(私有)。
模块系统的主要优点包括:
- 代码组织:将大型代码库分割成更小、更易管理的部分
- 封装:隐藏内部实现细节,只暴露必要的API
- 可复用性:模块可以在多个项目或文件中重复使用
- 命名空间:避免全局命名冲突
- 依赖管理:明确模块之间的依赖关系
JavaScript模块系统的演进
JavaScript最初并没有内置的模块系统,随着JavaScript应用规模的扩大,社区发展了多种模块规范:
- CommonJS:主要用于Node.js环境,使用
require()和module.exports - **AMD (Asynchronous Module Definition)**:主要用于浏览器环境,支持异步加载,如RequireJS
- **UMD (Universal Module Definition)**:兼容CommonJS和AMD,同时支持全局变量
- ES6模块:ES2015引入的官方模块系统,使用
import和export语法
CommonJS模块
CommonJS是Node.js默认的模块系统,使用同步加载方式。
CommonJS导出
在CommonJS中,可以使用module.exports或exports对象导出模块内容。
// math.js
// 导出单个值
module.exports = {
add: function(a, b) {
return a + b;
},
subtract: function(a, b) {
return a - b;
},
PI: 3.14159
};
// 或者分开展出
exports.add = function(a, b) {
return a + b;
};
exports.subtract = function(a, b) {
return a - b;
};
exports.PI = 3.14159;CommonJS导入
使用require()函数导入CommonJS模块。
// app.js
// 导入整个模块
const math = require('./math');
console.log(math.add(2, 3)); // 5
console.log(math.subtract(5, 2)); // 3
console.log(math.PI); // 3.14159
// 或者解构导入
const { add, subtract, PI } = require('./math');
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
console.log(PI); // 3.14159CommonJS的特点
- 同步加载:模块加载是阻塞的,适合服务器端环境
- 动态加载:可以在运行时根据条件加载模块
- 值拷贝:模块导出的是值的拷贝,而非引用
- 单例模式:每个模块只被加载一次,多次导入返回同一个实例
ES6模块
ES6模块是ECMAScript 2015引入的官方模块系统,现在已被所有现代浏览器和Node.js支持。
ES6导出
ES6模块支持两种导出方式:命名导出和默认导出。
命名导出
可以导出多个命名值,使用export关键字。
// math.js
// 导出函数
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// 导出变量
export const PI = 3.14159;
// 或者集中导出
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
return a / b;
}
const E = 2.71828;
export { multiply, divide, E };默认导出
每个模块只能有一个默认导出,使用export default关键字。
// calculator.js
// 默认导出对象
const calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
export default calculator;
// 或者默认导出函数
export default function(a, b) {
return a + b;
}
// 或者默认导出类
export default class Calculator {
constructor() {
this.PI = 3.14159;
}
add(a, b) {
return a + b;
}
}ES6导入
使用import关键字导入ES6模块。
导入命名导出
// app.js
// 导入所有命名导出
import * as math from './math.js';
console.log(math.add(2, 3)); // 5
console.log(math.subtract(5, 2)); // 3
// 或者解构导入指定内容
import { add, subtract, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
console.log(PI); // 3.14159
// 重命名导入
import { add as sum, subtract as difference } from './math.js';
console.log(sum(2, 3)); // 5
console.log(difference(5, 2)); // 3导入默认导出
// app.js
// 导入默认导出
import calculator from './calculator.js';
console.log(calculator.add(2, 3)); // 5
// 同时导入默认导出和命名导出
import calculator, { add, subtract } from './calculator.js';
console.log(calculator.multiply(2, 3)); // 6
console.log(add(2, 3)); // 5ES6模块的特点
- 静态加载:模块导入和导出在编译时确定,支持静态分析
- 异步加载:适合浏览器环境,可以并行加载模块
- 值引用:模块导出的是值的引用,而非拷贝
- 单例模式:每个模块只被加载一次
- 严格模式:ES6模块自动运行在严格模式下
模块的使用场景
在浏览器中使用ES6模块
在HTML中,可以使用<script type="module">标签来加载ES6模块。
<!DOCTYPE html>
<html>
<head>
<title>ES6 Modules</title>
</head>
<body>
<script type="module" src="app.js"></script>
</body>
</html>在Node.js中使用ES6模块
Node.js支持两种方式使用ES6模块:
- 使用.mjs扩展名:将文件扩展名改为.mjs
- 在package.json中设置type字段:
{
"type": "module"
}模块打包工具
对于复杂的应用,通常使用模块打包工具来处理模块依赖和优化代码:
- Webpack:功能强大的模块打包工具,支持多种模块系统
- Rollup:专注于ES6模块的打包工具,生成更小的代码
- Parcel:零配置的模块打包工具
- Vite:现代前端构建工具,支持快速的开发服务器
模块的最佳实践
- 保持模块单一职责:每个模块只负责一个功能领域
- 使用清晰的命名:模块名称应反映其功能
- 合理组织模块结构:根据功能和相关性组织模块文件
- 最小化导出:只导出必要的API,隐藏内部实现细节
- 使用默认导出还是命名导出:
- 默认导出:适合模块只导出一个主要功能
- 命名导出:适合模块导出多个相关功能
- 避免循环依赖:模块A依赖模块B,模块B又依赖模块A
- 使用相对路径:在导入模块时使用相对路径,避免硬编码绝对路径
- 考虑树摇优化:使用命名导出有利于树摇(tree-shaking),移除未使用的代码
模块间的通信
模块间共享状态
// state.js
let count = 0;
export function increment() {
count++;
}
export function decrement() {
count--;
}
export function getCount() {
return count;
}
// app.js
import { increment, decrement, getCount } from './state.js';
increment();
console.log(getCount()); // 1
increment();
console.log(getCount()); // 2
decrement();
console.log(getCount()); // 1动态导入
ES2020引入了动态导入,允许在运行时根据条件异步导入模块。
// app.js
// 动态导入模块
async function loadModule() {
try {
const math = await import('./math.js');
console.log(math.add(2, 3)); // 5
} catch (error) {
console.error('Failed to load module:', error);
}
}
loadModule();
// 根据条件导入
const moduleName = someCondition ? './moduleA.js' : './moduleB.js';
import(moduleName)
.then(module => {
// 使用模块
})
.catch(error => {
console.error('Failed to load module:', error);
});总结
JavaScript模块系统的发展经历了从社区规范到官方标准的过程:
- CommonJS:Node.js的默认模块系统,使用
require()和module.exports,同步加载 - ES6模块:官方模块系统,使用
import和export,静态加载,支持浏览器和Node.js
现代JavaScript开发中,ES6模块已成为主流,提供了更简洁的语法和更好的性能。在实际开发中,应根据项目需求选择合适的模块系统,并遵循模块设计的最佳实践。
练习
创建一个
utils.js模块,包含以下功能:- 一个
formatDate函数,将日期对象格式化为YYYY-MM-DD格式 - 一个
capitalize函数,将字符串的首字母大写 - 一个
debounce函数,实现防抖功能
- 一个
创建一个
app.js文件,导入utils.js模块并使用其中的功能。尝试在浏览器中使用ES6模块,创建一个简单的HTML页面,加载
app.js模块。尝试使用动态导入,根据用户交互条件加载不同的模块。
比较CommonJS和ES6模块的区别,分别创建两个版本的相同功能模块。