PJCHENder 未整理筆記

[JS] JavaScript 模組(ES Module)

2017-10-26

[JS] JavaScript 模組(ES Module)

@(JavaScript)[ES6, JavaScript]

keywords: ES Module, es, esm

TL;DR

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

匯出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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);
}

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

1
2
3
4
5
6
7
8
// 在另一隻檔案只需要使用 import{} 即可匯入
import {
deviceName,
mobilesOnSale,
offers,
logPrice,
logDeviceName,
} from './utils';

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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{} 即可:

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

預設匯出(Default Export)

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

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

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

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

1
2
3
import showPrice from './utils';

showPrice(1000);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的縮寫
};

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

1
2
3
4
5
6
7
8
9
10
// myPhone 會是物件
import myPhone from './utils';

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

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

logPrice(24900);

匯入一整個模組 import module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'lodash'; // 「執行」 lodash module 中內容,且不存成變數
import _ from 'lodash'; // 載入 default 並取名為 _

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

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

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

觀念

Imgur

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

基本使用

模組的匯入與匯出

匯出:使用 export {...}

1
2
3
4
5
6
// 匯出 calc.js
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

export { str, fn, num };

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

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

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

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

1
2
3
4
5
6
7
// 匯出 export.js
let price = 1000;
function onSale() {
price *= 0.9;
}

export { price, onSale };
1
2
3
4
5
6
7
8
// 匯入
import { price, onSale } from './export';

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

as:為變數建立重新命名

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

1
2
3
4
5
6
// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

export { str as name, fn, num as year };
1
2
3
4
5
// 匯入
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 的變量
1
2
3
4
// 匯出
var multiply = (x, y) => x * y;

export default multiply; // 等同於 export { multiply as default }
1
2
3
4
5
6
7
8
// 匯入

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

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

// 錯誤
export default var a = 1;

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

1
2
3
4
5
6
7
8
9
10
// 匯出 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} // 等同於上面兩句
1
2
3
4
5
6
7
// 匯入
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 …: 匯入該模組中的所有內容

匯入的內容會是一包物件

1
2
3
4
5
6
// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

export { str, fn, num };
1
2
3
4
5
// 匯入
import * as calc from './calc'; // calc 會是一包物件

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

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

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

1
2
3
4
5
6
7
8
// 匯出
var str = 'Michael';
var fn = (x, y) => x * y;
var num = 1958;

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

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

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

Code Style

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

@ airbnb 10.4

1
2
3
4
5
6
// bad
import foo from 'foo';
import { named1, named2 } from 'foo';

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

將所有的 import 放在最上方

@ airbnb 10.7

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

1
2
3
4
5
6
7
8
9
10
11
// 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 的名稱相同

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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

Common JS 用法

模組的匯入

1
2
// ES6 寫法:用 import
import { str, fn, num } from './calc';
1
2
3
4
5
6
7
8
9
10
// CommonJS 寫法:用 require
/**
* require 只會載入檔案一次然後存放在記憶體裡面, 所以不用怕效能的問題
* 如果載入的是元件,開頭不用加 './',如果載入的是檔案或路徑,需要加上 './'
* 如果載入的是檔案沒寫副檔名,預設會載 .js 檔
**/

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

模組的匯出

1
2
3
4
5
6
7
8
9
10
/**
* ES6 寫法:用 export
**/

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

export { str, fn, num };
1
2
3
4
5
6
7
8
9
10
11
/**
* CommonJS 寫法:用 module.exports
**/
const fn (options) = {
return (req, res, next) => {
// do something here
// console.log(options.name)
return next()
}
}
module.exports = fn

其他範例

ES6

1
2
3
4
5
6
// ES6 匯出
const sum = (a, b) => a + b;
const multiply = (a, b) => a + b;

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

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

Common JS

1
2
3
4
5
6
7
// 匯出
const sum = (a, b) => a + b;
const multiply = (a, b) => a + b;
module.exports.sum = sum; // CommonJS
module.exports.multiply = multiply;

module.exports = { sum, multiply }; // CommonJS
1
2
3
4
5
6
7
// 匯入
const math = require('./math');
const sum = require('./math').sum;
const multiply = require('./math').multiply;

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

匯出 Constructor

匯出前先 new

1
2
3
4
5
6
7
8
9
// Common JS 匯出 constructor
function SumIndex() {
this.index = 2;
this.sum = function () {
console.log(this.index + 2);
};
}

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

匯出整個 class

1
2
3
4
5
6
7
8
9
// Common JS 匯出 constructor
function SumIndex() {
this.index = 2;
this.sum = function () {
console.log(this.index + 2);
};
}

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

參考資料

掃描二維條碼,分享此文章