require()、import、import()是我們常用的引入模組的三種方式,程式碼中幾乎處處用到。如果對它們存在模糊,就會在工作過程中不斷產生困惑,更無法做到對它們的使用揮灑自如。今天我們來一起捋一下,它們之間有哪些區別?
一、前世今生
學一個東西,先弄清楚它為什麼會出現、它的發展歷史、它是做什麼的,是很有必要的。看似與技術無關,卻很有助於你對技術的理解。
require():
require()是CommonJS引入模組的函數。CommonJS是一種規範,主要用於nodejs,無法用於瀏覽器端。它是規範,而不是語法。本質上,它是在JS本身不支援模組的情況下,程式設計師發揮聰明才智,模擬出來的模組系統,在語言層面沒有對JS做任何改動。它的原理實際上還是立即執行函數(IIFE)。
import:
JS在設計之初極為簡單,沒有模組系統,但開發中對模組化的需求卻與日俱增。終於,ES6正式在語言標準的層面,引入了模組系統。import就是ES6的模組引入語句,作為ES6本身的語法,只要能寫ES6的地方,都能使用它。它最大的特點,是靜態引入,在編譯時完成模組載入。這帶來了載入效率高、靜態分析等好處。
require()對比import,類似於VUE之於JS,對比ES6之於ES5的關係。
import():
相比require(),import好處多多,卻也丟失了動態引入的優點,即在執行時根據實際需要選擇引入模組。怎麼辦呢?
在ES2020中,引入了import()函數,它和require()一樣是動態載入,不同的是,它是非同步的,返回一個Promise物件,而require()是同步的。
二、快取方式
(1)先來回答一個面試中常問的問題,一個模組多次引入,會執行幾次?
require():
// 2.js
console.log('模組執行開始');
let num = 1;
module.exports = { num };
console.log('模組執行結束');
// testRequire.js
let a = require('./2.js');
let b = require('./2.js');
console.log(typeof a);
console.log(a === b);
// 執行結果
// 模組執行開始
// 模組執行結束
// object
// true
import:
// 1.js
console.log('模組執行開始');
export let num = 1;
console.log('模組執行結束');
// testImport.js
import * as a from './1.js';
import * as b from './1.js';
console.log(typeof a);
console.log(a === b);
// 執行結果
// 模組開始執行
// 模組執行結束
// object
// true
import():
// 1.js
console.log('模組執行開始');
export let num = 1;
console.log('模組執行結束');
// testImportFunction.js
let a = await import('./1.js');
let b = await import('./1.js');
console.log(typeof a);
console.log(a === b);
// 執行結果
// 模組開始執行
// 模組執行結束
// object
// true
由此可見,它們三個,在程式碼中多次引入同一模組,模組都是隻會執行一次。並且,輸出結果完全相等(===),也就是指向同一個參照。
我們可以判斷,它們都是第一次執行後把輸出結果快取了起來,多次引入不會再次執行,而是直接返回輸出結果的參照。
(2)那麼,它們對輸出結果的快取方式一樣嗎?
require():
// 2.js
let num = 1;
let obj = {
num: 1
};
function add() {
num += 1;
obj.num += 1;
}
module.exports = { num, obj, add };
// testRequire.js
let a = require('./2.js');
console.log(a.num); // 1
console.log(a.obj.num); // 1
a.add();
console.log(a.num); // 1
console.log(a.obj.num); // 2
require的快取方式,是對輸出結果進行拷貝,而且是淺拷貝。值型別直接拷貝,參照型別拷貝記憶體地址。
import:
// 1.js
let num = 1;
let obj = {
num: 1
};
function add() {
num += 1;
obj.num += 1;
}
export { num, obj, add };
// testImport.js
import * as a from './1.js';
console.log(a.num); // 1
console.log(a.obj.num); // 1
a.add();
console.log(a.num); // 2
console.log(a.obj.num); // 2
import並不對輸出結果進行拷貝,而是直接指向輸出結果的參照。
import():
// 1.js
let num = 1;
let obj = {
num: 1
};
function add() {
num += 1;
obj.num += 1;
}
export { num, obj, add };
// testImportFunction.js
let a = await import('./1.js');
console.log(a.num); // 1
console.log(a.obj.num); // 1
a.add();
console.log(a.num); // 2
console.log(a.obj.num); // 2
import()也是一樣的,直接指向輸出結果的參照。
三、靜態?動態?
靜態引入:
所謂靜態引入,就是在編譯時引入,那麼就不能使用在執行時才能得到結果的語法結構了,比如不能包在if語句裡,參照路徑不能使變數和表示式,要求必須是字串字面量。
import就是靜態引入。
動態引入:
動態引入,就是在執行時引入。因此可以根據條件判斷來按需引入,參照路徑也可以寫成變數或表示式。
require()和import()都是動態引入。
四、同步?非同步?
require():
// 2.js
console.log('模組執行開始');
let num = 1;
for (var i = 0; i < 1000000000; i++) {
}
module.exports = { num };
console.log('模組執行結束');
// testRequire.js
let a = require('./2.js');
console.log('執行其他程式碼');
// 執行結果
// 模組執行開始
// 若干秒後...
// 模組執行結束
// 執行其他程式碼
require()引入模組,是同步的,但是因為是在伺服器端本地參照,同步引入完全沒有問題。
import:
// 1.js
console.log('模組執行開始');
let num = 1;
await new Promise(resolve => {
setTimeout(resolve, 3000);
});
export { num };
console.log('模組執行結束');
// testImport.js
import * as a from './1.js';
console.log('執行其他程式碼');
// 執行結果
// 模組執行開始
// 3秒後...
// 模組執行結束
// 執行其他程式碼
這兒讓我非常意外,也很困惑。都說import是非同步引入的,為什麼這兒的結果卻顯示它是同步的?哪位能解答我的疑惑?
import():
// 1.js
console.log('模組執行開始');
let num = 1;
await new Promise(resolve => {
setTimeout(resolve, 3000);
});
export { num };
console.log('模組執行結束');
// testImportFunction.js
import('./1.js');
console.log('執行其他程式碼');
// 執行結果
// 執行其他程式碼
// 模組執行開始
// 3秒後...
// 模組執行結束
沒問題,import()是非同步引入,返回的是一個Promise物件。
五、相互參照
require():
require()無法引入ES6模組,但可以使用import()函數來引入ES6模組。
import:
// 2.js
let num = 1;
let obj = {
num: 1
};
module.exports = { num, obj, add };
// testImport.js
import a from './2.js';
console.log(a.num); // 1
console.log(a.obj.num); // 1
import可以引入CommonJS模組,是把module.exports物件整體引入,類似於對exports default的接收,直接用一個變數來接收。
import():
// 2.js
let num = 1;
let obj = {
num: 1
};
module.exports = { num, obj, add };
// testImportFunction.js
let a = await import('./2.js');
console.log(a.default.num); // 1
console.log(a.default.obj.num); // 1
import()整體接收module.exports這個物件,並把它放在default屬性下。
就總結這些吧。本人水平非常有限,寫作主要是為了把自己學過的東西捋清楚。如有錯誤,還請指正,感激不盡。