Appearance
JavaScript 模块
什么是模块
模块(Module)是一种将代码组织为独立、可重用单元的方式。在 JavaScript 中,模块允许我们将代码分割成多个文件,每个文件包含特定功能的代码,然后通过导入(import)和导出(export)来使用这些代码。
模块的历史
JavaScript 最初并没有内置的模块系统,随着前端应用的复杂度增加,社区发展了多种模块系统:
- CommonJS:主要用于 Node.js,使用
require()和module.exports - AMD (Asynchronous Module Definition):主要用于浏览器,使用
define()和require() - UMD (Universal Module Definition):兼容 CommonJS 和 AMD
- ES6 模块:ES2015 引入的官方模块系统,使用
import和export
ES6 模块
1. 基本语法
导出(Export)
javascript
// 导出单个变量
export const name = 'John';
export const age = 30;
// 导出函数
export function greet() {
return 'Hello!';
}
// 导出类
export class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}`;
}
}
// 导出默认值
export default function() {
return 'Default export';
}
// 或者
export default {
name: 'John',
age: 30
};
// 或者
export default class {
constructor() {
this.name = 'John';
}
};导入(Import)
javascript
// 导入单个导出
import { name, age, greet, Person } from "./module.js";
console.log(name); // 'John'
console.log(age); // 30
console.log(greet()); // 'Hello!'
const person = new Person("Jane", 25);
console.log(person.greet()); // 'Hello, my name is Jane'
// 导入默认导出
import defaultExport from "./module.js";
console.log(defaultExport); // 取决于默认导出的内容
// 导入默认导出并命名
import myDefault from "./module.js";
// 同时导入默认导出和命名导出
import defaultExport, { name, age } from "./module.js";
// 导入所有命名导出为一个对象
import * as module from "./module.js";
console.log(module.name); // 'John'
console.log(module.age); // 30
console.log(module.greet()); // 'Hello!'
// 重命名导入
import { name as userName, age as userAge } from "./module.js";
console.log(userName); // 'John'
console.log(userAge); // 30
// 动态导入(返回 Promise)
import("./module.js")
.then((module) => {
console.log(module.name);
console.log(module.greet());
})
.catch((error) => {
console.error("Error loading module:", error);
});
// 使用 async/await 动态导入
async function loadModule() {
try {
const module = await import("./module.js");
console.log(module.name);
console.log(module.greet());
} catch (error) {
console.error("Error loading module:", error);
}
}
loadModule();2. 模块的特点
1. 严格模式
ES6 模块默认在严格模式下运行,不需要显式添加 'use strict;'。
javascript
// 模块中的代码默认在严格模式下运行
// 不需要添加 'use strict;'
// 以下代码会抛出错误,因为在严格模式下,未声明的变量不允许赋值
x = 10; // ReferenceError: x is not defined2. 独立的作用域
每个模块都有自己的作用域,模块内的变量、函数和类不会污染全局作用域。
javascript
// module1.js
const name = "John";
export { name };
// module2.js
const name = "Jane";
export { name };
// main.js
import { name as name1 } from "./module1.js";
import { name as name2 } from "./module2.js";
console.log(name1); // 'John'
console.log(name2); // 'Jane'
console.log(name); // ReferenceError: name is not defined(模块内的变量不会污染全局作用域)3. 静态分析
ES6 模块的导入和导出是静态的,在编译时就会被解析,而不是在运行时。这意味着:
- 导入和导出语句必须在模块的顶层
- 导入的名称必须是已知的
- 可以进行静态分析,如 tree-shaking(移除未使用的代码)
javascript
// 错误:导入语句不能在条件语句中
if (true) {
import { name } from "./module.js"; // SyntaxError: Unexpected token 'import'
}
// 错误:导出语句不能在条件语句中
if (true) {
export const name = "John"; // SyntaxError: Unexpected token 'export'
}4. 异步加载
ES6 模块在浏览器中是异步加载的,不会阻塞页面的渲染。
html
<!-- 在 HTML 中使用模块 -->
<script type="module" src="./main.js"></script>
<!-- 传统脚本是同步加载的 -->
<script src="./script.js"></script>3. 模块的导出方式
1. 命名导出
命名导出允许导出多个值,导入时需要使用相同的名称。
javascript
// 导出多个值
export const name = "John";
export const age = 30;
export function greet() {
return "Hello!";
}
// 或者先定义,再导出
const name = "John";
const age = 30;
function greet() {
return "Hello!";
}
export { name, age, greet };
// 重命名导出
export { name as userName, age as userAge, greet as sayHello };2. 默认导出
默认导出每个模块只能有一个,导入时可以使用任意名称。
javascript
// 默认导出函数
export default function() {
return 'Hello!';
}
// 默认导出对象
export default {
name: 'John',
age: 30
};
// 默认导出类
export default class {
constructor(name) {
this.name = name;
}
}
// 默认导出变量
const person = {
name: 'John',
age: 30
};
export default person;4. 模块的导入方式
1. 导入命名导出
javascript
// 导入特定的命名导出
import { name, age, greet } from "./module.js";
// 重命名导入
import { name as userName, age as userAge } from "./module.js";
// 导入所有命名导出
import * as module from "./module.js";
console.log(module.name);
console.log(module.age);
console.log(module.greet());2. 导入默认导出
javascript
// 导入默认导出
import defaultExport from "./module.js";
// 重命名默认导出
import myDefault from "./module.js";
// 同时导入默认导出和命名导出
import defaultExport, { name, age } from "./module.js";
// 或者
import { default as myDefault, name, age } from "./module.js";3. 动态导入
动态导入返回一个 Promise,允许在运行时按需加载模块。
javascript
// 动态导入
import("./module.js")
.then((module) => {
console.log(module.default);
console.log(module.name);
})
.catch((error) => {
console.error("Error loading module:", error);
});
// 使用 async/await
async function loadModule() {
try {
const module = await import("./module.js");
console.log(module.default);
console.log(module.name);
} catch (error) {
console.error("Error loading module:", error);
}
}
loadModule();
// 在条件语句中使用
if (condition) {
import("./module1.js").then((module) => {
// 使用 module1
});
} else {
import("./module2.js").then((module) => {
// 使用 module2
});
}CommonJS 模块
1. 基本语法
CommonJS 是 Node.js 使用的模块系统,使用 require() 导入模块,使用 module.exports 或 exports 导出模块。
导出
javascript
// 使用 module.exports 导出单个值
const name = "John";
const age = 30;
function greet() {
return "Hello!";
}
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// 导出单个值
module.exports = name;
// 导出对象
module.exports = {
name,
age,
greet,
Person,
};
// 使用 exports 导出多个值
exports.name = name;
exports.age = age;
exports.greet = greet;
exports.Person = Person;导入
javascript
// 导入整个模块
const module = require("./module.js");
console.log(module.name);
console.log(module.age);
console.log(module.greet());
// 导入特定的值
const { name, age, greet } = require("./module.js");
console.log(name);
console.log(age);
console.log(greet());
// 导入单个值
const name = require("./module.js");
console.log(name);2. CommonJS 模块的特点
- 动态加载:CommonJS 模块是在运行时动态加载的
- 同步加载:
require()是同步执行的 - 值拷贝:导入的是值的拷贝,而不是引用
- 模块缓存:模块会被缓存,多次
require()同一个模块只会执行一次
javascript
// module.js
let count = 0;
function increment() {
count++;
return count;
}
module.exports = {
count,
increment,
};
// main.js
const module = require("./module.js");
console.log(module.count); // 0
console.log(module.increment()); // 1
console.log(module.count); // 0(因为是值拷贝)
// 再次导入,会使用缓存
const module2 = require("./module.js");
console.log(module === module2); // trueES6 模块 vs CommonJS 模块
| 特性 | ES6 模块 | CommonJS 模块 |
|---|---|---|
| 语法 | import / export | require() / module.exports |
| 加载方式 | 静态(编译时) | 动态(运行时) |
| 执行环境 | 严格模式 | 非严格模式 |
| 加载时机 | 异步(浏览器) | 同步 |
| 值传递 | 引用 | 拷贝 |
| 适用环境 | 浏览器和 Node.js (ES2020+) | Node.js |
顶层 this | undefined | module.exports |
模块的应用场景
1. 代码组织
使用模块可以将代码组织为更小、更可管理的单元。
javascript
// utils/math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error("除数不能为 0");
}
return a / b;
}
// utils/string.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function reverse(str) {
return str.split("").reverse().join("");
}
// main.js
import * as math from "./utils/math.js";
import * as string from "./utils/string.js";
console.log(math.add(2, 3)); // 5
console.log(string.capitalize("hello")); // 'Hello'2. 依赖管理
模块系统使得依赖管理更加清晰,每个模块只需要声明它依赖的其他模块。
javascript
// services/api.js
import axios from "axios";
export async function fetchData(url) {
const response = await axios.get(url);
return response.data;
}
export async function postData(url, data) {
const response = await axios.post(url, data);
return response.data;
}
// services/user.js
import { fetchData, postData } from "./api.js";
const API_URL = "https://api.example.com/users";
export async function getUsers() {
return await fetchData(API_URL);
}
export async function getUser(id) {
return await fetchData(`${API_URL}/${id}`);
}
export async function createUser(user) {
return await postData(API_URL, user);
}
// main.js
import { getUsers, createUser } from "./services/user.js";
async function main() {
try {
const users = await getUsers();
console.log("Users:", users);
const newUser = await createUser({ name: "John", age: 30 });
console.log("New user:", newUser);
} catch (error) {
console.error("Error:", error);
}
}
main();3. 代码重用
模块使得代码可以在不同的项目中重用。
javascript
// components/Button.js
export function Button({ text, onClick }) {
const button = document.createElement("button");
button.textContent = text;
button.addEventListener("click", onClick);
return button;
}
// components/Card.js
export function Card({ title, content }) {
const card = document.createElement("div");
card.classList.add("card");
const cardTitle = document.createElement("h3");
cardTitle.textContent = title;
const cardContent = document.createElement("p");
cardContent.textContent = content;
card.appendChild(cardTitle);
card.appendChild(cardContent);
return card;
}
// main.js
import { Button } from "./components/Button.js";
import { Card } from "./components/Card.js";
// 使用 Button 组件
const button = Button({
text: "Click me",
onClick: () => console.log("Button clicked!"),
});
document.body.appendChild(button);
// 使用 Card 组件
const card = Card({
title: "Card Title",
content: "Card content goes here",
});
document.body.appendChild(card);4. 命名空间
模块可以作为命名空间使用,避免命名冲突。
javascript
// math/calculus.js
export function derivative(f, x, h = 0.0001) {
return (f(x + h) - f(x - h)) / (2 * h);
}
export function integral(f, a, b, n = 1000) {
const h = (b - a) / n;
let sum = 0;
for (let i = 0; i < n; i++) {
const x = a + i * h;
sum += f(x) * h;
}
return sum;
}
// math/algebra.js
export function solveLinearEquation(a, b) {
if (a === 0) {
throw new Error("系数 a 不能为 0");
}
return -b / a;
}
export function solveQuadraticEquation(a, b, c) {
const discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return [];
} else if (discriminant === 0) {
return [-b / (2 * a)];
} else {
const sqrtDiscriminant = Math.sqrt(discriminant);
return [
(-b + sqrtDiscriminant) / (2 * a),
(-b - sqrtDiscriminant) / (2 * a),
];
}
}
// main.js
import * as calculus from "./math/calculus.js";
import * as algebra from "./math/algebra.js";
// 使用微积分模块
const f = (x) => x * x;
console.log(calculus.derivative(f, 2)); // 4
console.log(calculus.integral(f, 0, 2)); // 2.666666666666667
// 使用代数模块
console.log(algebra.solveLinearEquation(2, -4)); // 2
console.log(algebra.solveQuadraticEquation(1, -3, 2)); // [2, 1]模块的最佳实践
1. 命名规范
- 模块名:使用小写字母,单词之间用连字符
-分隔 - 文件名:与模块名保持一致
- 导出名:使用驼峰命名法,对于默认导出,可以使用更具描述性的名称
javascript
// 好的命名
// utils/string-utils.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// main.js
import { capitalize } from "./utils/string-utils.js";2. 单一职责
每个模块应该只负责一个特定的功能,保持模块的简洁和专注。
javascript
// 好的做法:单一职责
// utils/validation.js
export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function validatePassword(password) {
return password.length >= 6;
}
// 不好的做法:多个职责
// utils/utils.js
export function validateEmail(email) {
// 验证邮箱
}
export function formatDate(date) {
// 格式化日期
}
export function calculateTotal(items) {
// 计算总价
}3. 导出方式
- 默认导出:用于模块的主要功能,如单个函数、类或对象
- 命名导出:用于模块的辅助功能,如多个相关的函数或常量
javascript
// 使用默认导出
// utils/date.js
function formatDate(date) {
return new Intl.DateTimeFormat("zh-CN").format(date);
}
export default formatDate;
// main.js
import formatDate from "./utils/date.js";
console.log(formatDate(new Date()));
// 使用命名导出
// utils/validators.js
export function validateEmail(email) {
// 验证邮箱
}
export function validatePassword(password) {
// 验证密码
}
export function validatePhone(phone) {
// 验证手机号
}
// main.js
import { validateEmail, validatePassword } from "./utils/validators.js";4. 导入方式
- 按需导入:只导入需要的内容,减少打包体积
- 使用别名:当导入的名称与本地变量冲突时,使用别名
- 动态导入:对于大型模块,使用动态导入按需加载
javascript
// 按需导入
import { validateEmail } from "./utils/validators.js";
// 使用别名
import { validateEmail as validateEmailAddress } from "./utils/validators.js";
// 动态导入
async function handleSubmit() {
const { validateEmail } = await import("./utils/validators.js");
// 使用 validateEmail
}5. 模块路径
- 相对路径:用于导入项目内部的模块
- 绝对路径:用于导入第三方库或使用模块解析器(如 webpack)配置的路径
javascript
// 相对路径
import { capitalize } from "./utils/string-utils.js";
import { formatDate } from "../utils/date.js";
// 绝对路径(导入第三方库)
import axios from "axios";
import React from "react";
// 绝对路径(使用模块解析器)
import { validateEmail } from "@/utils/validators.js";6. 错误处理
- 静态导入:使用 try-catch 捕获导入错误
- 动态导入:使用 Promise 的 catch 方法捕获错误
javascript
// 静态导入错误处理
try {
import { validateEmail } from "./utils/validators.js";
} catch (error) {
console.error("Error importing module:", error);
}
// 动态导入错误处理
import("./utils/validators.js")
.then((module) => {
// 使用模块
})
.catch((error) => {
console.error("Error loading module:", error);
});模块的工具和库
1. 模块打包工具
- webpack:功能强大的模块打包工具,支持多种模块系统
- Rollup:专注于 ES6 模块的打包工具,生成更小的 bundle
- Parcel:零配置的模块打包工具
- Vite:基于 ES6 模块的开发服务器和构建工具
2. 模块解析
- Node.js 模块解析:根据
NODE_PATH和node_modules目录解析模块 - webpack 模块解析:可以配置别名和解析规则
- TypeScript 模块解析:支持多种模块解析策略
3. 模块热替换
模块热替换(Hot Module Replacement, HMR)允许在不刷新页面的情况下更新模块。
javascript
// webpack 中的 HMR
if (module.hot) {
module.hot.accept("./module.js", () => {
console.log("Module updated!");
// 更新依赖该模块的代码
});
}总结
模块是 JavaScript 中组织代码的重要方式,ES6 模块是官方推荐的模块系统,它具有以下特点:
- 静态分析:导入和导出在编译时解析,支持 tree-shaking
- 严格模式:默认在严格模式下运行
- 独立作用域:模块内的变量不会污染全局作用域
- 异步加载:在浏览器中异步加载,不阻塞页面渲染
- 引用传递:导入的是值的引用,而不是拷贝
CommonJS 模块是 Node.js 使用的模块系统,它具有以下特点:
- 动态加载:在运行时解析导入
- 同步加载:
require()是同步执行的 - 值拷贝:导入的是值的拷贝
- 模块缓存:模块会被缓存
通过合理使用模块,我们可以:
- 提高代码的可维护性
- 促进代码的重用
- 减少命名冲突
- 优化构建大小
- 提高开发效率
模块系统是现代 JavaScript 开发的基础,掌握模块的使用是成为一名优秀的 JavaScript 开发者的必要条件。