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 [default] class/function/variable ...
  • 獨立匯出
    • 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…)