Javascript 導出和導入

2023-02-17 10:53 更新

導出(export)和導入(import)指令有幾種語法變體。

在上一節(jié),我們看到了一個簡單的用法,現(xiàn)在讓我們來探索更多示例吧。

在聲明前導出

我們可以通過在聲明之前放置 ?export? 來標記任意聲明為導出,無論聲明的是變量,函數(shù)還是類都可以。

例如,這里的所有導出均有效:

// 導出數(shù)組
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;
  }
}

導出 class/function 后沒有分號

注意,在類或者函數(shù)前的 export 不會讓它們變成 函數(shù)表達式。盡管被導出了,但它仍然是一個函數(shù)聲明。

大部分 JavaScript 樣式指南都不建議在函數(shù)和類聲明后使用分號。

這就是為什么在 export class 和 export function 的末尾不需要加分號:

export function sayHi(user) {
  alert(`Hello, ${user}!`);
}  // 在這里沒有分號 ;

導出與聲明分開

另外,我們還可以將 export 分開放置。

下面的例子中,我們先聲明函數(shù),然后再導出它們:

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

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

export {sayHi, sayBye}; // 導出變量列表

……從技術上講,我們也可以把 export 放在函數(shù)上面。

Import *

通常,我們把要導入的東西列在花括號 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. 現(xiàn)代的構建工具(webpack 和其他工具)將模塊打包到一起并對其進行優(yōu)化,以加快加載速度并刪除未使用的代碼。
  2. 比如說,我們向我們的項目里添加一個第三方庫 say.js,它具有許多函數(shù):

    //  say.js
    export function sayHi() { ... }
    export function sayBye() { ... }
    export function becomeSilent() { ... }

    現(xiàn)在,如果我們只在我們的項目里使用了 say.js 中的一個函數(shù):

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

    ……那么,優(yōu)化器(optimizer)就會檢測到它,并從打包好的代碼中刪除那些未被使用的函數(shù),從而使構建更小。這就是所謂的“搖樹(tree-shaking)”。

  3. 明確列出要導入的內容會使得名稱較短:?sayHi()? 而不是 ?say.sayHi()?。
  4. 導入的顯式列表可以更好地概述代碼結構:使用的內容和位置。它使得代碼支持重構,并且重構起來更容易。

Import “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!

Export “as”

導出也具有類似的語法。

我們將函數(shù)導出為 ?hi? 和 ?bye?:

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

現(xiàn)在 hi 和 bye 是在外面使用時的正式名稱:

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

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

Export default

在實際中,主要有兩種模塊。

  • 包含庫或函數(shù)包的模塊,像上面的 ?say.js?。
  • 聲明單個實體的模塊,例如模塊 ?user.js? 僅導出 ?class User?。

大部分情況下,開發(fā)者傾向于使用第二種方式,以便每個“東西”都存在于它自己的模塊中。

當然,這需要大量文件,因為每個東西都需要自己的模塊,但這根本不是問題。實際上,如果文件具有良好的命名,并且文件夾結構得當,那么代碼導航(navigation)會變得更容易。

模塊提供了一個特殊的默認導出 export default 語法,以使“一個模塊只做一件事”的方式看起來更好。

將 export default 放在要導出的實體前:

//  user.js
export default class User { // 只需要添加 "default" 即可
  constructor(name) {
    this.name = name;
  }
}

每個文件應該只有一個 export default

……然后將其導入而不需要花括號:

//  main.js
import User from './user.js'; // 不需要花括號 {User},只需要寫成 User 即可

new User('John');

不用花括號的導入看起來很酷。剛開始使用模塊時,一個常見的錯誤就是忘記寫花括號。所以,請記住,import 命名的導出時需要花括號,而 import 默認的導出時不需要花括號。

命名的導出 默認的導出
export class User {...} export default class User {...}
import {User} from ... import User from ...

從技術上講,我們可以在一個模塊中同時有默認的導出和命名的導出,但是實際上人們通常不會混合使用它們。模塊要么是命名的導出要么是默認的導出。

由于每個文件最多只能有一個默認的導出,因此導出的實體可能沒有名稱。

例如,下面這些都是完全有效的默認的導出:

export default class { // 沒有類名
  constructor() { ... }
}
export default function(user) { // 沒有函數(shù)名
  alert(`Hello, ${user}!`);
}
// 導出單個值,而不使用變量
export default ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

不指定名稱是可以的,因為每個文件只有一個 export default,因此不帶花括號的 import 知道要導入的內容是什么。

如果沒有 default,這樣的導出將會出錯:

export class { // Error!(非默認的導出需要名稱)
  constructor() {}
}

“default” 名稱

在某些情況下,?default? 關鍵詞被用于引用默認的導出。

例如,要將函數(shù)與其定義分開導出:

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

// 就像我們在函數(shù)之前添加了 "export default" 一樣
export {sayHi as default};

或者,另一種情況,假設模塊 user.js 導出了一個主要的默認的導出和一些命名的導出(這種情況很少見,但確實會發(fā)生):

//  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; // 默認的導出
new User('John');

我應該使用默認的導出嗎?

命名的導出是明確的。它們確切地命名了它們要導出的內容,因此我們能從它們獲得這些信息,這是一件好事。

命名的導出會強制我們使用正確的名稱進行導入:

import {User} from './user.js';
// 導入 {MyUser} 不起作用,導入名字必須為 {User}

……對于默認的導出,我們總是在導入時選擇名稱:

import User from './user.js'; // 有效
import MyUser from './user.js'; // 也有效
// 使用任何名稱導入都沒有問題

因此,團隊成員可能會使用不同的名稱來導入相同的內容,這不好。

通常,為了避免這種情況并使代碼保持一致,可以遵從這條規(guī)則,即導入的變量應與文件名相對應,例如:

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

但是,一些團隊仍然認為這是默認的導出的嚴重缺陷。因此,他們更傾向于始終使用命名的導出。即使只導出一個東西,也仍然使用命名的導出,而不是默認的導出。

這也使得重新導出(見下文)更容易。

重新導出

“重新導出(Re-export)”語法 export ... from ... 允許導入內容,并立即將其導出(可能是用的是其他的名字),就像這樣:

export {sayHi} from './say.js'; // 重新導出 sayHi

export {default as User} from './user.js'; // 重新導出 default

為什么要這樣做?我們看一個實際開發(fā)中的用例。

想象一下,我們正在編寫一個 “package”:一個包含大量模塊的文件夾,其中一些功能是導出到外部的(像 NPM 這樣的工具允許我們發(fā)布和分發(fā)這樣的 package,但我們不是必須要去使用它們),并且其中一些模塊僅僅是供其他 package 中的模塊內部使用的 “helpers”。

文件結構可能是這樣的:

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 導出了我們希望在包中提供的所有功能。

這樣做是因為,其他使用我們包的開發(fā)者不應該干預其內部結構,不應該搜索我們包的文件夾中的文件。我們只在 auth/index.js 中導出必要的部分,并保持其他內容“不可見”。

由于實際導出的功能分散在 package 中,所以我們可以將它們導入到 auth/index.js,然后再從中導出它們:

//  auth/index.js

// 導入 login/logout 然后立即導出它們
import {login, logout} from './helpers.js';
export {login, logout};

// 將默認導出導入為 User,然后導出它
import User from './user.js';
export {User};
...

現(xiàn)在使用我們 package 的人可以 import {login} from "auth/index.js"。

語法 export ... from ... 只是下面這種導入-導出的簡寫:

//  auth/index.js
// 重新導出 login/logout
export {login, logout} from './helpers.js';

// 將默認導出重新導出為 User
export {default as User} from './user.js';
...

export ... from 與 import/export 相比的顯著區(qū)別是重新導出的模塊在當前文件中不可用。所以在上面的 auth/index.js 示例中,我們不能使用重新導出的 login/logout 函數(shù)。

重新導出默認導出

重新導出時,默認導出需要單獨處理。

假設我們有一個 user.js 腳本,其中寫了 export default class User,并且我們想重新導出類 User

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

我們可能會遇到兩個問題:

  1. ?export User from './user.js'? 無效。這會導致一個語法錯誤。
  2. 要重新導出默認導出,我們必須明確寫出 ?export {default as User}?,就像上面的例子中那樣。

  3. ?export * from './user.js'? 重新導出只導出了命名的導出,但是忽略了默認的導出。
  4. 如果我們想將命名的導出和默認的導出都重新導出,那么需要兩條語句:

    export * from './user.js'; // 重新導出命名的導出
    export {default} from './user.js'; // 重新導出默認的導出

重新導出一個默認導出的這種奇怪現(xiàn)象,是某些開發(fā)者不喜歡默認導出,而是喜歡命名的導出的原因之一。

總結

這是我們在本節(jié)和前面章節(jié)中介紹的所有 export 類型:

你可以閱讀并回憶它們的含義來進行自查:

  • 在聲明一個 class/function/… 之前:
    • ?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'; // 在文件底部導入

在實際開發(fā)中,導入通常位于文件的開頭,但是這只是為了更加方便。

請注意在 {...} 中的 import/export 語句無效。

像這樣的有條件的導入是無效的:

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

……但是,如果我們真的需要根據(jù)某些條件來進行導入呢?或者在某些合適的時間?例如,根據(jù)請求(request)加載模塊,什么時候才是真正需要呢?

我們將在下一章節(jié)中學習動態(tài)導入。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號