JavaScript ES Module import/export 基础语法与用法
深入浅出讲解 JavaScript ES Module 的基础语法与核心用法,涵盖命名导出与导入、默认导出、实时只读绑定机制、动态导入以及现代开发中的常见避坑指南。
ES Module(通常简称为 ESM)是 ECMAScript 官方在 ES6(ES2015)中正式引入的 JavaScript 原生模块化规范。它从语言标准层面统一了此前由社区方案(如 CommonJS、AMD 等)割裂的模块体系,使同一套代码在现代浏览器与 Node.js 环境中都具备了开箱即用的模块导入与导出能力。本文将直接梳理 import 与 export 的基本语法、工作机制、常见混淆点以及在实际开发中的规范用法,帮助你在工程中准确使用 ES 模块。
一、ES Module 的运行机制与生命周期
理解 ES Module 的语法之前,先明确其核心设计特征至关重要:ES Module 是静态声明的。这意味着模块的依赖关系在代码实际执行之前(即编译/静态解析阶段)就已经确定。与之相对,CommonJS 的 require() 是运行时动态调用的函数。
正因为静态化特征,所有的顶层 import 和 export 语句必须位于模块作用域的最外层,不能嵌套在 if 条件分支、循环语句或函数体中。这种约束为现代构建工具(如 Rollup、Vite、Webpack)提供了可靠的死代码消除(Tree Shaking)基础。
当宿主环境(浏览器或 Node.js)加载 ES 模块时,通常会经历三个明确的分离阶段:构建、实例化与求值。
这三个步骤的分离带来了重要特性:在实例化阶段,变量与函数的导出位置与导入位置就已经完成了指针连接,即使求值尚未开始。这种机制使得 ES Module 天然具备处理循环引用的能力,并在安全性上严格保证了导入变量不可被外部修改。
二、命名导出与导入(Named Export / Import)
命名导出适用于一个模块需要向外提供多个变量、函数或类的场景。导入方在使用时,必须通过相同的标识符名称进行精确匹配。
1. 声明时直接导出
最直接的方式是在变量、常量、函数或类的声明语句前添加 export 关键字:
// mathUtils.js
export const PI = 3.1415926;
export const E = 2.7182818;
export function add(a, b) {
return a + b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
}
2. 统一列表导出
也可以先在模块内部完成定义,再通过末尾的列表形式集中向外暴露:
// formatUtils.js
const DEFAULT_LOCALE = 'zh-CN';
function formatDate(date) {
return new Intl.DateTimeFormat(DEFAULT_LOCALE).format(date);
}
function formatCurrency(amount) {
return `¥${amount.toFixed(2)}`;
}
// 统一导出语句
export { formatDate, formatCurrency };
许多初学者在书写 export { a, b } 时,会误以为大括号包裹的是一个常规的 JavaScript 对象字面量。这种理解是错误的。这里的 { ... } 是 ES Module 专用的导出语法结构,而不是对象定义。因此:
- 写作
export { key: value }会直接触发SyntaxError语法错误。 - 不能在花括号内执行赋值表达式或动态计算。
3. 使用 as 关键字重命名
当导出的名称与当前上下文冲突,或者希望提供更简短、更语义化的接口时,可以使用 as 关键字进行别名设置。重命名既可以在导出端进行,也可以在导入端进行:
// 导出端重命名
const internalApiEndpoint = 'https://api.example.com/v1';
export { internalApiEndpoint as API_URL };
// 导入端重命名
import { API_URL as endpoint } from './config.js';
console.log(endpoint);
4. 命名空间整体导入
如果目标模块导出了很多接口,逐一在花括号中列出可能显得冗长,此时可以使用 * as Alias 语法将所有命名导出打包为一个只读命名空间对象:
// 导入 mathUtils.js 的所有导出
import * as MathTools from './mathUtils.js';
console.log(MathTools.PI);
console.log(MathTools.add(10, 20));
需要注意的是,即使通过 MathTools 对象进行访问,该对象也是被冻结的,不可向其添加新属性,也不能修改其已有属性的值。
三、默认导出与导入(Default Export / Import)
默认导出主要用于模块本身的核心功能非常单一、或者整个模块本质上就是一个独立组件或类的场景(例如 React 组件、单一工具类库)。
每个 JavaScript 文件在 ES Module 规范下最多只能有一个 export default。
// Logger.js
export default class Logger {
info(message) {
console.log(`[INFO] ${message}`);
}
}
在导入默认导出时,语法上不使用花括号,且导入方可以完全自由地决定局部变量的名称:
// main.js - 自由指定标识符名称
import CustomLogger from './Logger.js';
const logger = new CustomLogger();
logger.info('系统启动完成');
默认导出与命名导出并非互斥关系,同一个模块内可以同时包含一个默认导出和若干个命名导出:
// request.js
export const TIMEOUT = 5000;
export default function request(url) {
return fetch(url);
}
// 消费端混合导入
import request, { TIMEOUT } from './request.js';
四、核心原理:实时只读绑定(Live Binding)
在理解 ES 模块时,最容易被低估却又最本质的概念是实时只读绑定(Live Binding)。这是 ES Module 与 CommonJS 的分水岭。
在 CommonJS 中,当你执行 const { count } = require('./counter') 时,本质是将模块导出的值做了一次浅拷贝并存入当前作用域。如果 counter 模块内部随后更新了变量的值,导入方持有的拷贝变量不会发生任何变动。
而在 ES Module 中,import 引入的标识符并非独立的新变量,而是建立了一条直接映射到底层模块内部内存槽位的只读动态指针:
// state.js
export let count = 0;
export function increment() {
count += 1;
}
// main.js
import { count, increment } from './state.js';
console.log(count); // 输出: 0
increment();
console.log(count); // 输出: 1 (自动获取最新内存值)
// 试图在导入方修改值:
count = 99; // 抛出错误: TypeError: Assignment to constant variable.
从上面的例子可以看出两点核心事实:
- 只要原模块内部更新了被导出的变量,所有引入该变量的消费端模块都会实时获取到最新的计算值。
- 导入方所获得的绑定是严格只读的。导入方无法在自身代码中直接对
count进行重赋值。如果需要变更状态,必须调用导出模块提供的操作函数。
五、中转聚合导出与动态 import()
1. 模块聚合导出(Re-exporting)
在构建大型前端项目或 SDK 类库时,通常会在某个目录下设立一个 index.js 作为该目录对外导出的统一门面(Facade)。ES Module 提供了简洁的聚合语法,无需先 import 进当前文件再 export 出去:
// components/index.js
// 1. 导出 button.js 中的所有命名导出
export * from './Button.js';
// 2. 将 Modal.js 的默认导出重新作为命名导出暴露
export { default as Modal } from './Modal.js';
// 3. 仅挑选部分功能中转
export { Tabs, TabPane } from './Tabs.js';
使用这种模式,外部调用方只需引入一层目录即可,避免了深层路径导入带来的脆弱性:
// 简洁的统一引入
import { Button, Modal, Tabs } from './components/index.js';
2. 动态 import() 实现按需加载
虽然标准静态 import 必须置于顶层,但 ES2020 引入了函数形式的 import(specifier) 语法。它返回一个 Promise,允许在代码运行过程中按需加载特定模块:
// 某个按钮点击后按需加载富文本编辑器
async function handleOpenEditor() {
try {
const { Editor } = await import('./heavyEditor.js');
const editorInstance = new Editor('#editor-container');
editorInstance.render();
} catch (error) {
console.error('加载编辑器模块失败:', error);
}
}
动态 import() 的核心应用包括:单页应用路由的按需懒加载、根据用户权限条件分支加载功能模块、以及多语言国际化包的异步切换。
六、环境配置与高频排查指南
在日常使用中,不同宿主环境对 ES Module 的解释方式略有差异,以下是排查高频问题的实用指引:
1. 浏览器环境下的使用
在原生 HTML 中引入 ES 模块,必须在 <script> 标签中指明 type="module":
<!-- 启用 ES Module 支持 -->
<script type="module" src="./main.js"></script>
带有 type="module" 的脚本具有以下行为变化:
- 自动遵循 defer 延迟执行:脚本会在后台下载,等待 HTML DOM 解析完毕后再按序执行。
- 默认启用严格模式:模块顶部无需再手动书写
'use strict'。 - 独立顶层作用域:模块内部顶层声明的变量不会挂载到全局
window对象上。 - CORS 跨域约束:通过 HTTP 请求加载的模块受同源策略限制,本地调试时应通过本地 HTTP 服务器(如
npx serve或 Vite)打开,不能使用file:///协议。
2. Node.js 环境下的配置
Node.js 默认将 .js 文件视作 CommonJS 规范。要在 Node.js 原生运行 ES Module,推荐在项目的 package.json 中添加配置项:
{
"name": "my-project",
"type": "module"
}
配置该项后,项目目录下的所有 .js 文件均自动按照 ES Module 解析。如果不修改 package.json,也可以直接将文件后缀名命名为 .mjs。
3. 常见报错与解决办法
-
报错:
SyntaxError: Cannot use import statement outside a module
原因:Node.js 或浏览器在 CommonJS 模式下试图执行import语法。
解决:浏览器端为<script>添加type="module"属性;Node.js 端在package.json中声明"type": "module"。 -
报错:
TypeError: Assignment to constant variable
原因:尝试对import进来的变量进行二次赋值。
解决:ES Module 导入是只读的。如需修改,请通过原模块暴露的更新函数进行修改。 -
报错:浏览器报 404 或找不到模块(Relative references must start with...)
原因:在没有打包工具(如 Webpack、Vite)介入的原生浏览器环境下,相对路径导入必须以./、../开头,且必须显式书写.js扩展名,浏览器不会自动猜测文件名补齐。
七、总结
JavaScript ES Module 的 import 和 export 建立了清晰、安全且可预测的原生代码复用机制。掌握它的核心要点在于:
- 静态解析决定了架构效率:顶层声明使得依赖关系在执行前固化,赋予现代工具卓越的打包与树摇优化能力。
- 命名与默认各得其所:多个函数或工具优先使用命名导出,保持名称一致与 IDE 智能检索;单一核心功能或组件可采用默认导出。
- 始终牢记实时绑定:导入并非一份静止的快照,而是对源模块作用域的只读映射指针。理解这一点,就能规避模块状态流转中的诸多疑难问题。
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53








