2023 年 1 月 18 日

导出和导入

导出和导入指令有几种语法变体。

在上一篇文章中,我们看到了一个简单的用法,现在让我们探索更多示例。

在声明之前导出

我们可以通过在声明之前放置 export 来标记任何声明为已导出,无论它是变量、函数还是类。

例如,这里所有导出都是有效的

// export an array
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

// export a constant
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// export a class
export class User {
  constructor(name) {
    this.name = name;
  }
}
导出类/函数后不加分号

请注意,在类或函数之前使用 export 不会使其成为 函数表达式。它仍然是函数声明,尽管已导出。

大多数 JavaScript 样式指南不建议在函数和类声明后加分号。

这就是为什么 export classexport function 末尾不需要分号

export function sayHi(user) {
  alert(`Hello, ${user}!`);
}  // no ; at the end

导出除声明之外的内容

此外,我们可以单独放置 export

这里我们首先声明,然后导出

// 📁 say.js
function sayHi(user) {
  alert(`Hello, ${user}!`);
}

function sayBye(user) {
  alert(`Bye, ${user}!`);
}

export {sayHi, sayBye}; // a list of exported variables

…或者,从技术上讲,我们也可以将 export 放在函数之上。

导入 *

通常,我们会在花括号 import {...} 中放置要导入的内容列表,如下所示

// 📁 main.js
import {sayHi, sayBye} from './say.js';

sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!

但是如果有大量内容要导入,我们可以使用 import * as <obj> 将所有内容作为对象导入,例如

// 📁 main.js
import * as say from './say.js';

say.sayHi('John');
say.sayBye('John');

乍一看,“导入所有内容”似乎是一件很酷的事情,写起来很短,我们为什么还要明确列出需要导入的内容?

嗯,有几个原因。

  1. 明确列出要导入的内容可以提供更短的名称:sayHi() 而不是 say.sayHi()
  2. 明确的导入列表可以更好地概述代码结构:什么内容在什么位置使用。它使代码支持和重构变得更容易。
不要害怕导入太多内容

现代构建工具(如 webpack 等)将模块捆绑在一起并对其进行优化以加快加载速度。它们还会删除未使用的导入内容。

例如,如果你从一个庞大的代码库中 import * as library,然后只使用几个方法,那么未使用的导入内容 将不会包含 在经过优化的捆绑包中。

导入“as”

我们还可以使用 as 以不同的名称导入内容。

例如,让我们将 sayHi 导入到局部变量 hi 中以简化代码,并将 sayBye 导入为 bye

// 📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';

hi('John'); // Hello, John!
bye('John'); // Bye, John!

导出“as”

export 具有类似的语法。

让我们将函数导出为 hibye

// 📁 say.js
...
export {sayHi as hi, sayBye as bye};

现在 hibye 是供外部人员使用的正式名称,可用于导入

// 📁 main.js
import * as say from './say.js';

say.hi('John'); // Hello, John!
say.bye('John'); // Bye, John!

导出默认值

在实践中,主要有两种类型的模块。

  1. 包含库和函数包的模块,如上文的 say.js
  2. 声明单个实体的模块,例如模块 user.js 仅导出 class User

通常,第二种方法更受欢迎,这样每个“事物”都驻留在自己的模块中。

自然,这需要很多文件,因为每个文件都需要自己的模块,但这根本不是问题。实际上,如果文件命名得当并按文件夹组织,代码导航会变得更容易。

模块提供特殊的 export default(“默认导出”)语法,以使“每个模块一件事”的方式看起来更好。

在要导出的实体前放置 export default

// 📁 user.js
export default class User { // just add "default"
  constructor(name) {
    this.name = name;
  }
}

每个文件只能有一个 export default

…然后在不使用大括号的情况下导入它

// 📁 main.js
import User from './user.js'; // not {User}, just User

new User('John');

不使用大括号的导入看起来更漂亮。在开始使用模块时,一个常见的错误是完全忘记大括号。因此,请记住,import 需要大括号进行命名导出,而对于默认导出则不需要。

命名导出 默认导出
export class User {...} export default class User {...}
import {User} from ... import User from ...

从技术上讲,我们可以在单个模块中同时进行默认导出和命名导出,但在实践中,人们通常不会将它们混合在一起。模块要么有命名导出,要么有默认导出。

由于每个文件最多只能有一个默认导出,因此导出的实体可能没有名称。

例如,这些都是完全有效的默认导出

export default class { // no class name
  constructor() { ... }
}
export default function(user) { // no function name
  alert(`Hello, ${user}!`);
}
// export a single value, without making a variable
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

不提供名称是可以的,因为每个文件只有一个 export default,因此不使用大括号的 import 知道要导入什么。

如果没有 default,这样的导出将产生错误

export class { // Error! (non-default export needs a name)
  constructor() {}
}

“默认”名称

在某些情况下,default 关键字用于引用默认导出。

例如,要将函数与其定义分开导出

function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// same as if we added "export default" before the function
export {sayHi as default};

或者,另一种情况,假设模块 user.js 导出一个主要的“默认”内容和一些命名的内容(这种情况很少见,但确实存在)

// 📁 user.js
export default class User {
  constructor(name) {
    this.name = name;
  }
}

export function sayHi(user) {
  alert(`Hello, ${user}!`);
}

以下是如何导入默认导出以及命名的导出

// 📁 main.js
import {default as User, sayHi} from './user.js';

new User('John');

最后,如果将所有内容 * 作为对象导入,则 default 属性恰好是默认导出

// 📁 main.js
import * as user from './user.js';

let User = user.default; // the default export
new User('John');

反对默认导出的言论

命名导出是明确的。它们准确地命名了它们导入的内容,因此我们从它们那里获得了该信息;这是一件好事。

命名导出迫使我们使用完全正确的名称进行导入

import {User} from './user.js';
// import {MyUser} won't work, the name must be {User}

…而对于默认导出,我们在导入时总是选择名称

import User from './user.js'; // works
import MyUser from './user.js'; // works too
// could be import Anything... and it'll still work

因此,团队成员可以使用不同的名称导入相同的内容,这是不好的。

通常,为了避免这种情况并保持代码一致性,有一条规则,即导入的变量应对应于文件名,例如

import User from './user.js';
import LoginForm from './loginForm.js';
import func from '/path/to/func.js';
...

尽管如此,一些团队认为这是默认导出的一个严重缺点。因此,他们更喜欢始终使用命名导出。即使只导出一个内容,它仍然在没有 default 的情况下以一个名称导出。

这也使重新导出(见下文)变得更容易一些。

重新导出

“重新导出”语法 export ... from ... 允许导入内容并立即导出它们(可能使用另一个名称),如下所示

export {sayHi} from './say.js'; // re-export sayHi

export {default as User} from './user.js'; // re-export default

为什么需要这样做?让我们看一个实际的用例。

想象一下,我们正在编写一个“包”:一个包含许多模块的文件夹,其中一些功能导出到外部(如 NPM 的工具允许我们发布和分发此类包,但我们不必使用它们),并且许多模块只是“帮助程序”,用于其他包模块的内部使用。

文件结构可以如下所示

auth/
    index.js
    user.js
    helpers.js
    tests/
        login.js
    providers/
        github.js
        facebook.js
        ...

我们希望通过单个入口点公开包功能。

换句话说,希望使用我们包的人应该只从“主文件”auth/index.js导入。

像这样

import {login, logout} from 'auth/index.js'

“主文件”auth/index.js导出了我们希望在包中提供的全部功能。

我们的想法是,外部人员(使用我们包的其他程序员)不应干预其内部结构,在我们的包文件夹中搜索文件。我们只在auth/index.js中导出必要的内容,并将其余内容隐藏起来,以免窥探。

由于实际导出的功能分散在包中,我们可以将其导入auth/index.js并从中导出

// 📁 auth/index.js

// import login/logout and immediately export them
import {login, logout} from './helpers.js';
export {login, logout};

// import default as User and export it
import User from './user.js';
export {User};
...

现在,我们包的用户可以import {login} from "auth/index.js"

语法export ... from ...只是此类导入导出的较短表示法

// 📁 auth/index.js
// re-export login/logout
export {login, logout} from './helpers.js';

// re-export the default export as User
export {default as User} from './user.js';
...

export ... fromimport/export的显着区别在于,重新导出的模块在当前文件中不可用。因此,在上述auth/index.js示例中,我们无法使用重新导出的login/logout函数。

重新导出默认导出

重新导出时,默认导出需要单独处理。

假设我们有user.js,其中包含export default class User,并且希望重新导出它

// 📁 user.js
export default class User {
  // ...
}

我们可能会遇到两个问题

  1. export User from './user.js'不起作用。这会导致语法错误。

    要重新导出默认导出,我们必须编写export {default as User},如上例所示。

  2. export * from './user.js'仅重新导出命名导出,但忽略默认导出。

    如果我们希望重新导出命名导出和默认导出,则需要两个语句

    export * from './user.js'; // to re-export named exports
    export {default} from './user.js'; // to re-export the default export

重新导出默认导出的这种奇怪之处是某些开发人员不喜欢默认导出而更喜欢命名导出的原因之一。

总结

以下是我们在本文和前文中介绍的所有类型的export

你可以通过阅读它们并回忆它们的含义来检查自己

  • 在声明类/函数/…之前
    • 导出 [默认] 类/函数/变量 ...
  • 独立导出
    • export {x [as y], ...}.
  • 重新导出
    • export {x [as y], ...} from "module"
    • export * from "module"(不会重新导出默认值)。
    • export {default [as y]} from "module"(重新导出默认值)。

导入

  • 导入命名导出
    • import {x [as y], ...} from "module"
  • 导入默认导出
    • import x from "module"
    • import {default as x} from "module"
  • 全部导入
    • import * as obj from "module"
  • 导入模块(其代码运行),但不要将任何导出内容分配给变量
    • import "module"

我们可以将 import/export 语句放在脚本的顶部或底部,这无关紧要。

因此,从技术上讲,此代码是正确的

sayHi();

// ...

import {sayHi} from './say.js'; // import at the end of the file

实际上,导入通常位于文件开头,但这只是为了更方便。

请注意,如果在 {...} 内,则 import/export 语句不起作用。

这样的条件导入不起作用

if (something) {
  import {sayHi} from "./say.js"; // Error: import must be at top level
}

…但是,如果我们真的需要有条件地导入某些内容怎么办?或者在正确的时间导入?例如,在真正需要时根据请求加载模块?

我们将在下一篇文章中看到动态导入。

教程地图

评论

在评论之前先阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,要插入多行代码 - 将其包装在 <pre> 标记中,要插入 10 行以上的代码 - 使用沙盒 (plnkrjsbincodepen…)