跳至主要内容

[JS] JavaScript 模組(ES Module, ESM)

TL;DR

  • module 的內容只有在第一次被 import 的時候會被執行(evaluated),而且它會是 singleton,也就是說,如果 export 的是物件,這個物件只會有一個,在任何 module 都會改到同一個物件(參考:A module code is evaluated only the first time when imported)。
  • 如果是在瀏覽中使用 ESModule,則每個 script 都會以與 defer 相同的方式執行。

ESM

// default 後面直接是 JavaScript 物件,可以接 key-value
export default { foo: 'bar' };
import esm from './modules'; // { foo: 'bar' }

// named imports and exports
// 代表匯出 foo 和 bar 這兩個變數,不能用 key-value pair
export { foo, bar };
import { foo, bar } from './modules';

// re-export
export { foo, bar } from './modules';

實名匯出 - 直接定義變數並匯出

匯出:

// util.js
// 直接定義並匯出變數
export const deviceName = 'iPhone';
export const mobilesOnSale = ['Samsung', 'Apple', 'Huawei'];
export const offers = {
priceCurrency: 'TWD',
price: '26,900',
};
export const logPrice = (price) => {
console.log('price: ', price);
};
export function logDeviceName(deviceName) {
console.log(deviceName);
}

匯入的名稱需要和匯出時相對應

// 在另一隻檔案只需要使用 import{} 即可匯入
import { deviceName, mobilesOnSale, offers, logPrice, logDeviceName } from './utils';

實名匯出 - 先定義好變數再匯出

也可以先把變數定義好,接著透過 export {} 把變數匯出,這種做法可以在匯出時透過 as 修改匯出的名稱:

// util.js
// 先定義好變數後匯出
const deviceName = 'iPhone';
const mobilesOnSale = ['Samsung', 'Apple', 'Huawei'];
const offers = {
priceCurrency: 'TWD',
price: '26,900',
};
const logPrice = (price) => {
console.log('price: ', price);
};
function logDeviceName(deviceName) {
console.log(deviceName);
}

// 匯出時可以進行名稱的修改
export { deviceName, mobilesOnSale, offers as productDetail, logPrice, logDeviceName };
注意

要特別留意這裡 export 後的大括號並不是物件,而是匯出用的語法,千萬不要在裡面使用 key-value 這樣的寫法!

匯入時同樣只需要根據匯出時的變數對應使用 import{ } 即可:

// 在另一隻檔案只需要使用 import{ } 即可匯入
import {
deviceName as device,
productDetail, // 要用匯出時的名稱
} from './utils';

預設匯出(Default Export)

當模組只有一個單一的 export 時才建議使用 export default,雖然可以但不建議同時使用 default export 又使用 named export

預設匯出的語法是在 export 後加上 defaultdefault 後則直接帶入你想要匯出的東西即可,例如:個變數:

const logPrice = (price) => {
console.log('price: ', price);
};
export default logPrice;

匯入的時候可以自己隨意取名,這裡我們取做 showPrice,就可以直接使用:

import showPrice from './utils';

showPrice(1000);

要特別留意的是,和實名匯出不同,如果你在 export default 後接的是 {} ,這個 {} 表示的就是物件,裡面放的就會是物件的屬性名稱和屬性值,因此不能再用 as 去修改名稱,例如:

const deviceName = 'iPhone';
const mobilesOnSale = ['Samsung', 'Apple', 'Huawei'];
const logPrice = (price) => {
console.log('price: ', price);
};

// export default 後直接帶入要匯出的東西
// 這裡是會直接匯出「物件」,因此不能在裡面使用 "as" 語法
export default {
deviceName, // deviceName: deviceName 的縮寫
mobilesOnSale, // mobilesOnSale: mobilesOnSale 的縮寫
logPrice, // logPrice: logPrice 的縮寫
};

匯入時也會是一個物件,若要使用裡面的資料,需要使用物件的方式來操作:

// myPhone 會是物件
import myPhone from './utils';

// 透過物件的方式操作
console.log('Device:', myPhone.deviceName);

// 也可以透過解構賦值將需要的屬性取出
const { logPrice } = myPhone;

logPrice(24900);
提示

和實名匯出的 export {} 不同, export default {} 後的 {} 表示的真的就是個物件。匯入時也是匯入整個物件。

匯入一整個模組 import module

import 'lodash'; // 「執行」 lodash module 中內容,且不存成變數
import _ from 'lodash'; // 載入 default 並取名為 _

import { map, reduce } from 'lodash'; // 只載入特定模組
import { map as _map } from 'lodash'; // 載入特定模組並重新命名
import _, { map } from 'lodash'; // 同時載入 default 與特定模組
import { default as _, map } from 'lodash'; // 效果同上

import * as _ from 'lodash'; // 載入全部的模組,並放到名為 _ 的 namespace 中

// 動態載入
import('./modules.js').then(({ default: DefaultExport, NamedExport }) => {
// do something with modules.
});

動態載入(Dynamic Import)

動態載入(Dynamic Import) @ pjchender > Webpack 學習筆記

觀念

Imgur

  • 在模組內的程式總是會套用 strict mode(因此不需要額外定義 "use strict"
  • 模組並沒有共享全域空間,每一個模組都有一個屬於它自己的作用域(scope),和其他模組溝通時,需要透過 export 來暴露變數,沒有匯出的函式是無法使用的
greet.js
// greet.js
function greet() {
console.log('Hello');
}
greet();
app.js
// app.js
/**
* 執行 app.js 時會出現 'Hello'
* 但是在 app.js 中並沒有辦法呼叫到 greet() 這個函式
**/
require('./greet.js');

基本語法

模組的匯入與匯出

匯出:使用 export {...}

// 匯出 calc.js
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

export { str, fn, num };

匯入: 搭配 import {...} from ...大括號裡面的變量名,必須與被匯入的模組接口名稱相同

// 匯入,{ } 裡的變量名要與被匯入的 module 中的變數對應
import { str, fn, num } from './calc';

console.log('import', str, num);
console.log(fn(3, 4));

另外,匯入 module 時實際上仍參照到原本的變數,因此呼叫 onSale 時,price 的值會改變:

// 匯出 export.js
let price = 1000;
function onSale() {
price *= 0.9;
}

export { price, onSale };
// 匯入
import { price, onSale } from './export';

console.log('price', price); // 1000
onSale();
console.log('price', price); // 900
onSale();
console.log('price', price); // 810

as:為變數建立重新命名

匯出或匯入時均可使用 as 重新命名模組:

// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

export { str as name, fn, num as year };
// 匯入
import { name, fn as multiply, year } from './calc';

console.log('import', name, year);
console.log(multiply(3, 2));

default

當匯入的 module 具有 default 時,就不需要知道原模組中輸出的函數名

  • 不論匯出的 function 有無名稱,匯入時都視為匿名函式;匯入時可以為該匿名函數指定任意名字
  • 匯入時不使用大括號 { }(一般的匯入在匯入時需使用 { } 指定要匯入的變數名)
  • export default 命令其實只是輸出一個 as default 的變量
// 匯出
var multiply = (x, y) => x * y;

export default multiply; // 等同於 export { multiply as default }
// 匯入

/**
* 等同於 import { default as multiply } from './calc';
* 因為是 "as" 所以名稱可以自取,不用是 multiply
**/
import multiply from './calc'; //
console.log(multiply(3, 2));
  • 由於 export default 命令其實只是輸出一個 as default 的變量,所以它後面不能跟變量聲明語句:
// 正確
export var a = 1;

// 正確
var a = 1;
export default a;

// 錯誤
export default var a = 1;

另外,可以同時使用 export { }export default同時匯出 default 和特定內容

// 匯出 export.js
let price = 1000;
function onSale() {
price *= 0.9;
}
let defaultText = 'This is default text';

export { price, onSale };
export default defaultText;
// export { defaultText as default, price, onSale} // 等同於上面兩句
// 匯入
import defaultValue, { price, onSale } from './export';

console.log('defaultValue', defaultValue); // This is default text
console.log('price', price); // 1000
onSale();
console.log('price', price); // 900

import * as ...: 匯入該模組中的所有內容

// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

export { str, fn, num };

匯入的內容會是一包物件:

// 匯入
import * as calc from './calc'; // calc 會是一包物件

console.log('import', calc.str, calc.num);
console.log(calc.fn(2, 6));

匯入(執行)模組但不賦予變數

使用 import './module' 可以匯入(執行)模組但不賦予變數:

// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

console.log('calc'); // 這行會被執行

export { str, fn, num };
import 'lodash'; // 載入(執行)整個模組但不賦予變數
import './calc'; //

console.log('import', num); // undefined

在瀏覽器中使用 ESM

在瀏覽器中使用 module 有幾個需要留意的地方:

  • 這種方式需要使用 serve 的方式,也就是只能用 HTTP(S) 的方式,不能直接點兩下用檔案的方式開啟。
  • 載入的 module path 一定要寫完整的路徑並加副檔名,不能省略 .js

在瀏覽器中使用 module 時,預設載入的方式和 defer 一樣,它們會等到 DOM 準備好後,才開始「依序」執行:

<body>
<main>
<!-- ... -->

<!-- module 會依照順序被執行 -->
<script type="module" src="main.js"></script>
<script type="module" src="./1.js"></script>
<script type="module" src="./2.js"></script>
</main>
</body>

如果這個 module 和其他的 module 間沒有相依的關係,也可以使用 async 的方式,但只能用 inline 的方式:

<!-- https://javascript.info/modules-intro#async-works-on-inline-scripts -->
<body>
<main>
<!-- ... -->

<script async type="module">
import { counter } from './analytics.js';
counter.count();
</script>
</main>
</body>

在 Node.js 中使用 ESM

請到這裡

其他範例

ES6

// ES6 匯出
const sum = (a, b) => a + b;
const multiply = (a, b) => a + b;

export default { sum, multiply };
export { sum, multiply };
// ES6 匯入
import math from './math'; // ES6,沒有用 { } ,表示匯入 default 內容

console.log(math.sum(3, 5));
console.log(math.multiply(3, 5));

匯出 Constructor

匯出前先 new

// Common JS 匯出 constructor
function SumIndex() {
this.index = 2;
this.sum = function () {
console.log(this.index + 2);
};
}

module.exports = new SumIndex();
// Common JS 匯入
const math = require('./math'); // 但是如果之後在重新載入這個 module,並不會在重新引用一次
math.sum();

匯出整個 class

// Common JS 匯出 constructor
function SumIndex() {
this.index = 2;
this.sum = function () {
console.log(this.index + 2);
};
}

module.exports = SumIndex;
// Common JS 匯入
const Math = require('./math'); // 但是如果之後在重新載入這個 module,並不會在重新引用一次
let math = new Math();
math.sum();

與 Common JS 語法的小比較

模組的匯入

// ES6 寫法:用 import
import { str, fn, num } from './calc';
// CommonJS 寫法:用 require
/**
* require 只會載入檔案一次然後存放在記憶體裡面, 所以不用怕效能的問題
* 如果載入的是檔案沒寫副檔名,預設會載 .js 檔
**/

const something = require('./something'); // 找不到 ./something 時自動尋找 ./something.js
const something = require('./something.js');
const something = require('something'); // 匯入 npm 模組

模組的匯出

/**
* ES6 寫法:用 export
**/

// profile.js
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

export { str, fn, num };
/**
* CommonJS 寫法:用 module.exports
**/
const fn (options) = {
return (req, res, next) => {
// do something here
// console.log(options.name)
return next()
}
}
module.exports = fn

Code Style

當從同一個路徑匯入時,不要拆成多個 import

@ airbnb 10.4

// bad
import foo from 'foo';
import { named1, named2 } from 'foo';

// good
import foo, { named1, named2 } from 'foo';

將所有的 import 放在最上方

@ airbnb 10.7

由於 import 具有變量提升(hoisted)的特性,因此應該放在文件最上方:

// bad
import foo from 'foo';
foo.init();

import bar from 'bar';

// good
import foo from 'foo';
import bar from 'bar';

foo.init();

命名規則

  • 小寫駝峰(camelCase):使用 export-default 時使用小寫駝峰命名與建檔
  • 大寫駝峰(PascalCase):當匯出的是純物件(pure object)、建構式(constructor)、類(class)、singleton 或 function library:

23.7 @ airbnb

檔名應該和 export default 的名稱相同

// file 1 contents
class CheckBox { ... }
export default CheckBox;

// file 2 contents
export default function fortyTwo() { return 42; }

// file 3 contents
export default function insideDirectory() {}

import CheckBox from './CheckBox';
import fortyTwo from './fortyTwo';
import insideDirectory from './insideDirectory';

23.6 A base filename should exactly match the name of its default export. @ airbnb

參考資料