ES模块语法
ES模块语法
ES模块是JavaScript的官方标准模块系统。本文将详细介绍import和export语法、默认导出与命名导出的区别,以及动态导入等高级特性。
ES模块简介
ES模块(ESM)是 ECMAScript 2015(ES6)引入的官方标准模块系统,用于组织和重用 JavaScript 代码。与传统的脚本文件不同,ES模块具有以下特点:
- 静态结构:导入和导出在编译时确定,而不是运行时
- 单例模式:每个模块只会被加载一次,无论被导入多少次
- 严格模式:模块自动运行在严格模式下,无需显式声明
"use strict"
- 顶级
this
为undefined
:模块顶级作用域中的this
值为undefined
,而不是全局对象 - 异步加载:模块可以异步加载,提高性能
导出(export)语法
ES模块使用 export
关键字将模块内部的变量、函数、类等导出,使其可以被其他模块导入和使用。
命名导出
命名导出允许从模块中导出多个值,每个值都有特定的名称。
单独导出
// 导出变量
export const name = 'JavaScript';
export let version = 'ES2022';
// 导出函数
export function sum(a, b) {
return a + b;
}
// 导出类
export class User {
constructor(name) {
this.name = name;
}
}
批量导出
const name = 'JavaScript';
let version = 'ES2022';
function sum(a, b) {
return a + b;
}
class User {
constructor(name) {
this.name = name;
}
}
// 批量导出
export { name, version, sum, User };
重命名导出
const name = 'JavaScript';
// 使用 as 关键字重命名导出
export { name as language, sum as add };
function sum(a, b) {
return a + b;
}
默认导出
每个模块可以有一个默认导出,使用 export default
语法。默认导出通常用于导出模块的主要功能。
// 导出默认函数
export default function(a, b) {
return a + b;
}
// 或导出默认类
export default class {
constructor(name) {
this.name = name;
}
}
// 或导出默认对象
export default {
name: 'JavaScript',
version: 'ES2022'
};
也可以将已声明的值作为默认导出:
function sum(a, b) {
return a + b;
}
// 将已声明的函数作为默认导出
export default sum;
混合导出
一个模块可以同时包含默认导出和命名导出:
// 默认导出
export default function sum(a, b) {
return a + b;
}
// 命名导出
export const name = 'JavaScript';
export class User {
constructor(name) {
this.name = name;
}
}
重新导出
模块可以重新导出从其他模块导入的内容,这对于创建聚合模块或公共 API 很有用:
// 重新导出其他模块的所有导出
export * from './math.js';
// 重新导出其他模块的特定导出
export { sum, multiply } from './math.js';
// 重新导出并重命名
export { sum as add, multiply } from './math.js';
// 重新导出默认导出为命名导出
export { default as math } from './math.js';
// 重新导出命名导出为默认导出
export { sum as default } from './math.js';
导入(import)语法
ES模块使用 import
关键字从其他模块导入值。
导入命名导出
// 导入特定的命名导出
import { name, version } from './module.js';
// 使用 as 关键字重命名导入
import { name as moduleName, version } from './module.js';
// 导入所有命名导出到一个对象中
import * as module from './module.js';
console.log(module.name); // 访问导入的值
导入默认导出
// 导入默认导出
import sum from './math.js';
// 可以使用任意名称接收默认导出
import calculate from './math.js';
混合导入
// 同时导入默认导出和命名导出
import sum, { multiply, divide } from './math.js';
// 导入默认导出和所有命名导出
import sum, * as mathModule from './math.js';
空导入
有时我们只需要执行模块中的代码,而不需要导入任何值:
// 仅执行模块,不导入任何内容
import './polyfills.js';
动态导入
ES2020 引入了动态导入功能,允许在运行时按需导入模块,而不是在编译时静态导入。
// 使用 import() 函数动态导入模块
async function loadModule() {
try {
// 返回一个 Promise
const module = await import('./dynamic-module.js');
// 使用导入的模块
console.log(module.default); // 访问默认导出
console.log(module.namedExport); // 访问命名导出
} catch (error) {
console.error('模块加载失败:', error);
}
}
// 或使用 Promise 链
import('./dynamic-module.js')
.then(module => {
// 使用导入的模块
module.default();
})
.catch(error => {
console.error('模块加载失败:', error);
});
动态导入的主要优势:
- 按需加载:只在需要时加载模块,减少初始加载时间
- 条件导入:基于条件决定是否加载模块
- 路径动态计算:模块路径可以在运行时计算
// 条件导入示例
async function loadLocaleMessages(locale) {
try {
// 动态计算模块路径
const messages = await import(`./locales/${locale}.js`);
return messages.default;
} catch (error) {
console.error(`无法加载语言包 ${locale}:`, error);
// 回退到默认语言
const defaultMessages = await import('./locales/en.js');
return defaultMessages.default;
}
}
模块加载顺序
ES模块的加载和执行遵循以下步骤:
- 构建:查找、下载并解析所有模块到模块记录中
- 实例化:为所有模块分配内存空间,并设置导出/导入引用(但尚未填充值)
- 求值:运行模块代码,填充内存中的值
这种方式确保了循环依赖的处理,但也可能导致一些意外行为,特别是在使用未初始化的值时。
模块路径解析
ES模块支持几种类型的模块说明符(路径):
相对路径
以 ./
或 ../
开头的路径,相对于当前模块:
import { sum } from './math.js';
import { User } from '../models/user.js';
绝对路径
以 /
开头的路径,相对于根目录:
import { config } from '/config/app.js';
URL 路径
完整的 URL 路径:
import { fetch } from 'https://cdn.example.com/js/fetch.js';
裸模块说明符
不包含路径信息的模块名称,通常用于导入 npm 包:
import React from 'react';
注意:在浏览器中,裸模块说明符需要通过导入映射(import maps)或构建工具(如 Webpack、Rollup)解析。
浏览器中使用 ES 模块
在 HTML 中,可以通过在 <script>
标签上添加 type="module"
属性来使用 ES 模块:
<!-- 内联模块 -->
<script type="module">
import { sum } from './math.js';
console.log(sum(1, 2));
</script>
<!-- 外部模块 -->
<script type="module" src="main.js"></script>
浏览器中的 ES 模块特点:
- 自动延迟:模块脚本会自动延迟执行,类似于添加了
defer
属性 - 跨域限制:模块必须遵循同源策略,或服务器必须提供适当的 CORS 头
- 缓存行为:模块只会被获取和执行一次,即使被多次导入
- 严格 MIME 类型:服务器必须使用正确的 MIME 类型(
text/javascript
或application/javascript
)提供 JavaScript 模块
Node.js 中使用 ES 模块
Node.js 支持两种模块系统:CommonJS(默认)和 ES 模块。使用 ES 模块有几种方式:
使用
.mjs
扩展名:// math.mjs export function sum(a, b) { return a + b; } // main.mjs import { sum } from './math.mjs'; console.log(sum(1, 2));
在
package.json
中设置"type": "module"
:{ "name": "my-package", "type": "module", "version": "1.0.0" }
这样,所有
.js
文件都会被视为 ES 模块。在 CommonJS 中使用动态导入:
// CommonJS 文件中 async function loadESModule() { const module = await import('./esmodule.mjs'); module.default(); }
ES 模块与 CommonJS 的区别
ES 模块和 CommonJS 有几个重要区别:
语法:
- ES 模块:
import
/export
- CommonJS:
require()
/module.exports
- ES 模块:
加载时机:
- ES 模块:静态分析,编译时确定
- CommonJS:动态加载,运行时确定
导入绑定:
- ES 模块:导入是实时绑定(live binding),导出值变化会反映到导入
- CommonJS:导入是值的拷贝,导出值变化不会影响已导入的值
默认导出:
- ES 模块:使用
export default
- CommonJS:使用
module.exports = ...
- ES 模块:使用
顶级
this
:- ES 模块:
undefined
- CommonJS:
exports
对象
- ES 模块:
最佳实践
使用 ES 模块时的一些最佳实践:
- 保持模块小而专注:每个模块应该只做一件事,并做好
- 使用命名导出:命名导出提供更好的可发现性和重构支持
- 避免副作用:模块应该尽量避免在导入时执行副作用
- 使用一致的命名约定:为文件和导出使用一致的命名约定
- 明确导入:优先使用具名导入,而不是导入整个模块
- 使用动态导入优化性能:对于大型或不常用的功能,考虑使用动态导入
- 避免循环依赖:虽然 ES 模块支持循环依赖,但最好避免使用
总结
ES 模块是 JavaScript 的官方标准模块系统,提供了强大而灵活的方式来组织和重用代码。通过静态导入/导出、动态导入、命名导出和默认导出等特性,ES 模块满足了现代 JavaScript 应用程序的需求。
随着浏览器和 Node.js 对 ES 模块的广泛支持,它已成为 JavaScript 生态系统中不可或缺的一部分,为开发者提供了一种标准化的方式来构建模块化、可维护的应用程序。