在上一篇文章中,我们认识了模块,本文则继续学习导入和导出指令,以及动态导出指令。
导出和导入
原文链接 导出和导入指令有几种语法变体:
- 在声明前导出
- 导出与声明分开
- Import *
- Import "as"
- Export "as"
- Export default
- 重新导出
下面我们挨个学!!
在声明前导出
- 可以在任意"声明"前加
export
导出:变量、函数、类都行。 - 这类导出属于"命名导出"(named export)。
示例:
js
// 导出变量(数组)
export let months = ['Jan', 'Feb', 'Mar', 'Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
// 导出 const
export const MODULES_BECAME_STANDARD_YEAR = 2015;
// 导出类
export class User {
constructor(name) {
this.name = name;
}
}
// 导出函数(声明式)
export function sayHi(user) {
alert(`Hello, ${user}!`);
} // 这里无需分号
- 导入命名导出时用花括号并按名称匹配:
js
import { months, MODULES_BECAME_STANDARD_YEAR, User, sayHi } from './module.js';
为什么## export function/class
后面没有分号?
export
修饰的是"声明",不会把它们变成"表达式"。 函数声明、类声明本身就不需要分号;只有表达式语句通常以分号结束。
对比:
js
// 声明(不需要分号)
export function f() {}
export class C {}
// 表达式(通常以分号结束,或靠 ASI)
export const g = function () {};
export const D = class {};
导出与声明分开
可以先声明,后在尾部集中 export
,便于统一管理导出接口。
js
// say.js
function sayHi(user) { alert(`Hello, ${user}!`); }
function sayBye(user) { alert(`Bye, ${user}!`); }
export { sayHi, sayBye }; // 导出列表
技术上也可以把 export
写在函数上方;二者等价。团队通常选择一种风格保持一致。
Import * as obj form "..."
这个意思是把一个模块的所有导出收集到命名空间对象上:
js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
通常不推荐这种"通通导入",为什么呢?
- 构建优化(tree-shaking)对"显式命名导入"更友好,能更好地移除未使用代码。
- 调用名更简洁:
sayHi()
而非say.sayHi()
- 显式列出依赖能更清晰地表达模块边界,便于重构
导入导出重命名:as
导入重命名:
js
import { sayHi as hi, sayBye as bye } from './say.js';
hi('John');
bye('John');
导出重命名:
js
// say.js
export { sayHi as hi, sayBye as bye };
// 使用
import * as say from './say.js';
say.hi('John');
say.bye('John');
Export default
用于"一个模块只暴露一个主要实体"的场景:
js
// user.js
export default class User {
constructor(name) { this.name = name; }
}
// 导入(无花括号,自选名称)
import User from './user.js';
new User('John');
一个文件最多一个默认导出,可无名称:
js
export default class { /* ... */ }
export default function (user) { alert(`Hello, ${user}!`); }
export default ['Jan', 'Feb', 'Mar', /* ... */];
错误示例(非默认导出必须有名):
js
export class { /* Error */ }
也可用"分离写法"导出默认:
js
function sayHi(user) { alert(`Hello, ${user}!`); }
export { sayHi as default }; // 等价于 export default sayHi
同时存在默认与命名导出(不推荐频繁混用,但语法允许):
js
// user.js
export default class User { /* ... */ }
export function sayHi(user) { /* ... */ }
// 导入默认与命名
import { default as User, sayHi } from './user.js';
// 或者
import * as user from './user.js';
const User = user.default;
那么什么时候用默认导出,什么时候用命名导出呢?
-
命名导出更"显式",强制一致的名称,便于工具与重构。
-
默认导出名称可随意,团队容易出现不同命名,可能影响一致性。
-
常见团队约定:
- 若使用默认导出,导入名与文件名保持一致(提高一致性)
- 或者干脆团队规范"只用命名导出",即便模块只导出一个东西
重新导出
"重新导出"让你在一个模块中把其他模块的导出"转手"导出,常用于创建单一入口(facade)或整理 API 表面,而无需在中间模块里显式使用这些标识符。
1.核心目的:模块封装(Encapsulation)与接口抽象(Interface Abstraction)
在软件工程中,封装是将实现细节隐藏起来,只暴露一个清晰、稳定的公共接口的过程。这对于构建可维护、可扩展和解耦的系统至关重要。
export ... from ...
语法是实现这一目标的关键工具,特别是在创建 "包"(Packages)或 "库"(Libraries)时。一个包通常由多个内部模块组成,但消费者(使用该包的开发者)不应该、也不需要了解其内部文件结构。
index.js
或类似的主入口文件在这种设计模式中扮演了 "门面"(Facade)的角色。 门面模式是一种结构型设计模式,它为一组复杂的子系统提供一个统一的、简化的接口。
export ... from ...
语法正是实现门面模式的专用语言特性。它允许入口文件从包的内部模块中聚合(aggregate)并导出(export)指定的公共 API,而无需在入口文件自身的作用域中创建任何本地绑定。
示例:
javascript
// 📁 my-package/index.js (门面文件)
// 从内部模块重新导出公共 API。 // 这些被重新导出的绑定在 `index.js` 文件内部是不可用的。
export { a, b } from './internal/moduleA.js';
export { default as C } from './internal/moduleB.js';
export * from './internal/moduleC.js'; // 只重新导出命名导出,会忽略默认导出
使用者(Consumer):
javascript
// 消费者只与稳定、公开的门面进行交互。
import { a, C } from 'my-package';
这样做的好处:
- API 稳定性 :我们可以自由地重构包的内部文件结构(例如,将
a
从moduleA.js
移动到moduleA_v2.js
),只需要更新index.js
中的from
路径即可,而不会破坏消费者的代码。 - 关注点分离:入口文件只负责定义公共接口,不包含任何实现逻辑。这使得 API 的结构一目了然。
- 防止内部依赖泄露 :消费者被引导只依赖于官方的入口,避免了他们直接导入内部模块(如
import { a } from 'my-package/internal/moduleA.js'
),这种不稳定的深度导入会使重构变得异常困难。
2. 与 import
/export
的技术差异
export ... from ...
不仅仅是 import
和 export
的语法糖,它在语义上有本质区别。
场景一:import
后再 export
javascript
// 📁 index.js
import { utility } from './utils.js';
// ... 这里的其他代码可以使用 'utility' ...
export { utility };
执行过程:
./utils.js
模块被解析和评估。utility
绑定被导入到index.js
的模块作用域(Module Scope)中。- 此时,
utility
是index.js
的一个本地绑定,可以在该文件中被调用或引用。 export
语句创建一个指向这个本地绑定的导出项。
场景二:export ... from ...
(Re-export)
javascript
// 📁 index.js
export { utility } from './utils.js';
//'utility' 在这里是不可用的。如果尝试使用,会抛出 ReferenceError。
执行过程:
./utils.js
模块被解析。export ... from ...
语句在index.js
的导出列表 (Export List)中创建一个条目,该条目直接链接到./utils.js
模块的utility
绑定。- 关键区别 :
utility
绑定不会 被添加到index.js
的模块作用域中。它只是一个传递(pass-through)或代理(proxy)的导出。这使得index.js
本身更加轻量,因为它不持有对这些绑定的直接引用。
3. 对默认导出的处理 (export default
)
ECMAScript 模块规范对命名导出(named exports)和默认导出(default export)进行了区分。default
实际上是一个特殊的导出名。
-
export { default as User } from './user.js'
- 分析 :这条语句非常明确。它指示模块加载器:"从
./user.js
中获取名为default
的导出,并以User
这个名字将其添加到当前模块的导出列表中。" - 这是重新导出
default
导出的标准且唯一明确的方式,因为它遵循了与重命名命名导出 (export { oldName as newName }
) 一致的逻辑。
- 分析 :这条语句非常明确。它指示模块加载器:"从
-
export * from './user.js'
(Star Export)- 规范定义 :根据 ECMA-262 规范,
export * from 'module'
(星号导出)会遍历指定模块的所有命名导出 ,并将它们添加到当前模块的导出列表中。这个过程明确排除了default
导出。 - 设计原因 :这种设计的背后有几个原因。其一,如果星号导出也包括
default
,那么应该如何命名它呢?自动命名可能会导致冲突或不可预测的行为。其二,default
导出通常被认为是一个模块的"主要"或"唯一"导出,规范鼓励对其进行显式处理,而不是通过通配符隐式处理。
- 规范定义 :根据 ECMA-262 规范,
-
组合导出 (Combined Re-export)
因此,要完整地重新导出一个模块的所有内容(命名和默认),需要两条语句的组合,这并非语言的"怪癖",而是其设计规则的直接体现:
javascript// Re-exports all named exports. export * from './user.js'; // Explicitly re-exports the default export. export { default } from './user.js';
注意:
import/export
有位置无关性:import
和export
语句是"静态的" 。 "静态"在这里意味着,JavaScript 引擎在执行 任何代码之前,会先进行一个"链接"阶段。在这个阶段,它会扫描所有模块,找出所有的import
和export
语句,并建立起模块之间的依赖关系图。它会确定哪个变量是从哪个模块导入的,哪个变量要被导出。正因为import
和export
在代码执行前就已经被处理完毕,所以它们写在文件的顶部还是底部,对于程序最终的运行结果来说,没有任何区别。
import/export
必须在顶层作用域:import
和export
必须位于模块的顶层。 这意味着你不能把它们放在代码块({...}
)中,比如if
语句、for
循环、或者函数内部。
动态导入
动态导入(Dynamic Import)通过 import()
表达式实现,与传统的静态导入不同,它可以在运行时按需加载模块。
静态导入 vs 动态导入
上文中其实已经提到了静态导入的限制
静态导入的限制:
javascript
// ❌ 这些都不被允许
import ... from getModuleName(); // 路径必须是字符串字面量
if (condition) {
import ...; // 不能在条件语句中使用
}
{
import ...; // 不能在代码块中使用
}
动态导入的优势:
javascript
// ✅ 这些都是可以的
let modulePath = './myModule.js';
import(modulePath).then(module => {
// 使用模块
});
// 条件加载
if (condition) {
const module = await import('./conditionalModule.js');
}
// 运行时决定加载哪个模块
const moduleName = userChoice ? './moduleA.js' : './moduleB.js';
const module = await import(moduleName);
动态导入的基本用法
1. Promise 形式
javascript
import('./myModule.js')
.then(module => {
module.someFunction();
})
.catch(err => {
console.error('模块加载失败:', err);
});
2. async/await 形式
javascript
async function loadModule() {
try {
const module = await import('./myModule.js');
module.someFunction();
} catch (err) {
console.error('模块加载失败:', err);
}
}
处理不同的导出类型
具名导出
javascript
// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// 使用
const { add, subtract } = await import('./math.js');
console.log(add(5, 3)); // 8
默认导出
javascript
// utils.js
export default function greet(name) {
return `Hello, ${name}!`;
}
// 使用方式1
const module = await import('./utils.js');
const greet = module.default;
console.log(greet('张三'));
// 使用方式2
const { default: greet } = await import('./utils.js');
console.log(greet('张三'));
混合导出
javascript
// api.js
export default class ApiClient { }
export const API_URL = 'https://api.example.com';
export function formatData(data) { }
// 使用
const { default: ApiClient, API_URL, formatData } = await import('./api.js');
实际应用场景
1. 代码分割和懒加载
javascript
// 只有当用户点击按钮时才加载重的图表库
async function showChart() {
const { Chart } = await import('./chartLibrary.js');
new Chart(document.getElementById('chart'), config);
}
2. 条件性功能加载
javascript
// 根据设备类型加载不同的功能
async function loadDeviceSpecificFeatures() {
if (window.innerWidth < 768) {
const mobileFeatures = await import('./mobileFeatures.js');
mobileFeatures.initMobileUI();
} else {
const desktopFeatures = await import('./desktopFeatures.js');
desktopFeatures.initDesktopUI();
}
}
3. 国际化
javascript
async function loadLanguage(lang) {
try {
const translations = await import(`./locales/${lang}.js`);
return translations.default;
} catch (err) {
// 回退到默认语言
const defaultTranslations = await import('./locales/en.js');
return defaultTranslations.default;
}
}
4. 插件系统
javascript
class PluginManager {
async loadPlugin(pluginName) {
try {
const plugin = await import(`./plugins/${pluginName}.js`);
return new plugin.default();
} catch (err) {
console.error(`无法加载插件 ${pluginName}:`, err);
return null;
}
}
}
重要注意事项
- 不是函数调用 :
import()
是特殊语法,不能被赋值或使用 call/apply
javascript
// ❌ 错误用法
const dynamicImport = import;
dynamicImport('./module.js'); // 不会工作
- 返回 Promise:始终返回 Promise,即使在同步环境中也是如此
- 模块缓存:同一个模块只会被加载一次,后续导入会返回缓存的实例
- 错误处理:始终要处理可能的加载错误
性能考虑
优点:
- 减少初始包大小:不立即需要的代码不会包含在初始bundle中
- 更快的首屏加载:只加载必要的代码
- 更好的用户体验:按需加载避免了不必要的网络请求
缺点:
- 网络延迟:动态加载需要额外的网络请求
- 加载状态处理:需要处理加载中的状态
动态导入是现代JavaScript中非常强大的特性,特别适合大型应用的性能优化和代码组织。它让我们能够构建更加高效、灵活的应用程序。