【JS】模块(二)

上一篇文章中,我们认识了模块,本文则继续学习导入和导出指令,以及动态导出指令。

导出和导入

原文链接 导出和导入指令有几种语法变体:

  1. 在声明前导出
  2. 导出与声明分开
  3. Import *
  4. Import "as"
  5. Export "as"
  6. Export default
  7. 重新导出

下面我们挨个学!!

在声明前导出

  • 可以在任意"声明"前加 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 稳定性 :我们可以自由地重构包的内部文件结构(例如,将 amoduleA.js 移动到 moduleA_v2.js),只需要更新 index.js 中的 from 路径即可,而不会破坏消费者的代码。
  • 关注点分离:入口文件只负责定义公共接口,不包含任何实现逻辑。这使得 API 的结构一目了然。
  • 防止内部依赖泄露 :消费者被引导只依赖于官方的入口,避免了他们直接导入内部模块(如 import { a } from 'my-package/internal/moduleA.js'),这种不稳定的深度导入会使重构变得异常困难。

2. 与 import/export 的技术差异

export ... from ... 不仅仅是 importexport 的语法糖,它在语义上有本质区别。

场景一:import 后再 export

javascript 复制代码
// 📁 index.js
import { utility } from './utils.js';
// ... 这里的其他代码可以使用 'utility' ...
export { utility };

执行过程:

  1. ./utils.js 模块被解析和评估。
  2. utility 绑定被导入到 index.js模块作用域(Module Scope)中。
  3. 此时,utilityindex.js 的一个本地绑定,可以在该文件中被调用或引用。
  4. export 语句创建一个指向这个本地绑定的导出项。

场景二:export ... from ... (Re-export)

javascript 复制代码
// 📁 index.js
export { utility } from './utils.js';
//'utility' 在这里是不可用的。如果尝试使用,会抛出 ReferenceError。

执行过程:

  1. ./utils.js 模块被解析。
  2. export ... from ... 语句在 index.js导出列表 (Export List)中创建一个条目,该条目直接链接到 ./utils.js 模块的 utility 绑定。
  3. 关键区别utility 绑定不会 被添加到 index.js 的模块作用域中。它只是一个传递(pass-through)或代理(proxy)的导出。这使得 index.js 本身更加轻量,因为它不持有对这些绑定的直接引用。

3. 对默认导出的处理 (export default)

ECMAScript 模块规范对命名导出(named exports)和默认导出(default export)进行了区分。default 实际上是一个特殊的导出名。

  1. export { default as User } from './user.js'

    • 分析 :这条语句非常明确。它指示模块加载器:"从 ./user.js 中获取名为 default 的导出,并以 User 这个名字将其添加到当前模块的导出列表中。"
    • 这是重新导出 default 导出的标准且唯一明确的方式,因为它遵循了与重命名命名导出 (export { oldName as newName }) 一致的逻辑。
  2. export * from './user.js' (Star Export)

    • 规范定义 :根据 ECMA-262 规范,export * from 'module'(星号导出)会遍历指定模块的所有命名导出 ,并将它们添加到当前模块的导出列表中。这个过程明确排除了 default 导出
    • 设计原因 :这种设计的背后有几个原因。其一,如果星号导出也包括 default,那么应该如何命名它呢?自动命名可能会导致冲突或不可预测的行为。其二,default 导出通常被认为是一个模块的"主要"或"唯一"导出,规范鼓励对其进行显式处理,而不是通过通配符隐式处理。
  3. 组合导出 (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 有位置无关性:importexport 语句是"静态的" 。 "静态"在这里意味着,JavaScript 引擎在执行 任何代码之前,会先进行一个"链接"阶段。在这个阶段,它会扫描所有模块,找出所有的 importexport 语句,并建立起模块之间的依赖关系图。它会确定哪个变量是从哪个模块导入的,哪个变量要被导出。正因为 importexport 在代码执行前就已经被处理完毕,所以它们写在文件的顶部还是底部,对于程序最终的运行结果来说,没有任何区别

import/export 必须在顶层作用域:importexport 必须位于模块的顶层。 这意味着你不能把它们放在代码块({...})中,比如 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;
    }
  }
}

重要注意事项

  1. 不是函数调用import() 是特殊语法,不能被赋值或使用 call/apply
javascript 复制代码
// ❌ 错误用法
const dynamicImport = import;
dynamicImport('./module.js'); // 不会工作
  1. 返回 Promise:始终返回 Promise,即使在同步环境中也是如此
  2. 模块缓存:同一个模块只会被加载一次,后续导入会返回缓存的实例
  3. 错误处理:始终要处理可能的加载错误

性能考虑

优点:

  • 减少初始包大小:不立即需要的代码不会包含在初始bundle中
  • 更快的首屏加载:只加载必要的代码
  • 更好的用户体验:按需加载避免了不必要的网络请求

缺点:

  • 网络延迟:动态加载需要额外的网络请求
  • 加载状态处理:需要处理加载中的状态

动态导入是现代JavaScript中非常强大的特性,特别适合大型应用的性能优化和代码组织。它让我们能够构建更加高效、灵活的应用程序。

相关推荐
不会算法的小灰15 分钟前
JavaScript基础详解
开发语言·javascript·udp
十一吖i5 小时前
vue3表格显示隐藏列全屏拖动功能
前端·javascript·vue.js
徐同保6 小时前
tailwindcss暗色主题切换
开发语言·前端·javascript
生莫甲鲁浪戴7 小时前
Android Studio新手开发第二十七天
前端·javascript·android studio
细节控菜鸡9 小时前
【2025最新】ArcGIS for JS 实现随着时间变化而变化的热力图
开发语言·javascript·arcgis
拉不动的猪10 小时前
h5后台切换检测利用visibilitychange的缺点分析
前端·javascript·面试
桃子不吃李子10 小时前
nextTick的使用
前端·javascript·vue.js
Devil枫12 小时前
HarmonyOS鸿蒙应用:仓颉语言与JavaScript核心差异深度解析
开发语言·javascript·ecmascript
惺忪979812 小时前
回调函数的概念
开发语言·前端·javascript
前端 贾公子12 小时前
Element Plus组件v-loading在el-dialog组件上使用无效
前端·javascript·vue.js