JavaScript ES Module import/export 基础语法与用法

作者:青柠时间:2026-10-30 18:47:38浏览:253

深入浅出讲解 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 模块时,通常会经历三个明确的分离阶段:构建、实例化与求值。

JavaScript ES Module import/export 基础语法与用法
图 1:ES Module 运行生命周期的三个阶段:构建、实例化与求值

这三个步骤的分离带来了重要特性:在实例化阶段,变量与函数的导出位置与导入位置就已经完成了指针连接,即使求值尚未开始。这种机制使得 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';
JavaScript ES Module import/export 基础语法与用法
图 2:命名导出与默认导出在语法、数量和命名灵活性上的对比

四、核心原理:实时只读绑定(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.

从上面的例子可以看出两点核心事实:

  1. 只要原模块内部更新了被导出的变量,所有引入该变量的消费端模块都会实时获取到最新的计算值。
  2. 导入方所获得的绑定是严格只读的。导入方无法在自身代码中直接对 count 进行重赋值。如果需要变更状态,必须调用导出模块提供的操作函数。
JavaScript ES Module import/export 基础语法与用法
图 3:ES Module 实时只读引用与 CommonJS 值拷贝在内存模型上的核心差别

五、中转聚合导出与动态 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 的解释方式略有差异,以下是排查高频问题的实用指引:

JavaScript ES Module import/export 基础语法与用法
图 4: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 建立了清晰、安全且可预测的原生代码复用机制。掌握它的核心要点在于:

  1. 静态解析决定了架构效率:顶层声明使得依赖关系在执行前固化,赋予现代工具卓越的打包与树摇优化能力。
  2. 命名与默认各得其所:多个函数或工具优先使用命名导出,保持名称一致与 IDE 智能检索;单一核心功能或组件可采用默认导出。
  3. 始终牢记实时绑定:导入并非一份静止的快照,而是对源模块作用域的只读映射指针。理解这一点,就能规避模块状态流转中的诸多疑难问题。
作者最新文章
相关文章
更多