50 種 ES6 模塊,面試被問(wèn)麻了
測(cè)驗(yàn) #1: 53%的答案正確
// index.mjs
import { default } from './module.mjs';
console.log(default);
// module.mjs
export default 'bar';
首先,讓我們記住各種導(dǎo)入和導(dǎo)出語(yǔ)法:
如果檢查表中的 Import 語(yǔ)法,就會(huì)發(fā)現(xiàn)沒(méi)有與我們的代碼相匹配的語(yǔ)法:
import { default } from ‘./module.mjs’;
因?yàn)榻故褂眠@種語(yǔ)法。測(cè)驗(yàn)代碼會(huì)出現(xiàn)以下錯(cuò)誤:
SyntaxError: Unexpected reserved word
在 import { default } from ‘./module.mjs’; 行中, default 是 export 的名稱,也是該作用域中的變量名稱,這是被禁止的,因?yàn)?nbsp;default 是一個(gè)保留字。解決方法很簡(jiǎn)單:
import { default as foo } from ‘./module.mjs’;
現(xiàn)在, default 是導(dǎo)出的名稱, foo 是變量的名稱。換句話說(shuō),如果你想在默認(rèn)導(dǎo)出中使用命名導(dǎo)入語(yǔ)法,就必須重命名它。就是這樣,非常簡(jiǎn)單!
測(cè)驗(yàn) #2:35% 的正確答案
// index.js
console.log('index.js');
import { sum } from './helper.js';
console.log(sum(1, 2));
// helper.js
console.log('helper.js');
export const sum = (x, y) => x + y;
沒(méi)有多少開(kāi)發(fā)人員知道的一個(gè)重要的細(xì)微差別是,導(dǎo)入是被提升的。也就是說(shuō),在引擎解析代碼時(shí),導(dǎo)入就會(huì)被加載。所有依賴項(xiàng)都將在代碼運(yùn)行前加載。
因此,我們將按照以下順序查看日志:
helper.js, index.js, 3
如果希望在導(dǎo)入聲明之前執(zhí)行某些代碼,可考慮將其移至單獨(dú)的文件中:
// new index.js
import './logs.js';
import { sum } from './helper.js';
console.log(sum(1, 2));
logs.js
console.log('index.js');
現(xiàn)在我們有了預(yù)期的輸出結(jié)果:
index.js, helper.js, 3
測(cè)驗(yàn) #3:42% 的正確答案
index.mjs
// index.mjs
import './module.mjs';
import { num } from './counter.mjs';
console.log('index num =', num);
module.mjs
// module.mjs
import { num } from './counter.mjs';
console.log('module num =', num);
counter.mjs
// counter.mjs
if (!globalThis.num) {
globalThis.num = 0;
}
export const num = ++globalThis.num;
Modules are singletons. 模塊是單例。
無(wú)論從同一位置或不同位置導(dǎo)入模塊多少次,模塊都只會(huì)被執(zhí)行和加載一次。換句話說(shuō),模塊實(shí)例只有一個(gè)。
測(cè)驗(yàn) #4:34% 的正確答案
index.mjs
// index.mjs
import './module.mjs?param=5;'
module.mjs
// module.mjs
console.log(import.meta.url);
這個(gè)問(wèn)題如果沒(méi)有對(duì)ES6有比較深的理解,就不太好回答出來(lái)。
根據(jù) MDN:
import.meta 對(duì)象為 JavaScript 模塊提供特定于上下文的元數(shù)據(jù)。它包含有關(guān)模塊的信息。
它返回一個(gè)帶有 url 屬性的對(duì)象,url 屬性表示模塊的基本 URL。對(duì)于外部腳本,url 將是獲取腳本的 URL;對(duì)于內(nèi)嵌腳本,url 將是包含腳本的文檔的基本 URL。
請(qǐng)注意,這將包括查詢參數(shù)和/或哈希值(即,跟在 "?" 或 "#" 之后的部分)。
測(cè)驗(yàn) #5:45% 的正確答案
index.js
import myCounter from './counter';
myCounter += 1;
console.log(myCounter);
counter.js
// counter.js
let counter = 5;
export default counter;
另一個(gè)大多數(shù)開(kāi)發(fā)者容易忽視的非常重要的點(diǎn)是,在導(dǎo)入模塊的作用域中,導(dǎo)入的變量表現(xiàn)得像常量。
為了使代碼正常工作,我們可以導(dǎo)出一個(gè)對(duì)象,例如,并更改其屬性。
測(cè)驗(yàn) #6:11%的正確答案
// index.mjs
import foo from './module.mjs';
console.log(typeof foo);
// module.mjs
foo = 25;
export default function foo() {}
首先,這:
export default function foo() {}
等于:
function foo() {}
export { foo as default }
這也等于:
function foo() {}
export default foo
現(xiàn)在是時(shí)候回想起函數(shù)是如何被提升的,以及變量的初始化總是在函數(shù)/變量聲明之后進(jìn)行。
引擎處理完模塊代碼后,看起來(lái)是這樣的:
function foo() {}
foo = 25;
export { foo as default }
因此,測(cè)驗(yàn)結(jié)果就是 number 。
測(cè)驗(yàn) #7:17%的正確答案
// index.mjs
import defaultFoo, { foo } from './module.mjs';
setTimeout(() => {
console.log(foo);
console.log(defaultFoo);
}, 2000);
// module.mjs
let foo = 'bar';
export { foo };
export default foo;
setTimeout(() => {
foo = 'baz';
}, 1000);
在大多數(shù)情況下,導(dǎo)入的數(shù)據(jù)是實(shí)時(shí)的。也就是說(shuō),如果導(dǎo)出的值發(fā)生了變化,這種變化會(huì)反映在導(dǎo)入的變量上。
但默認(rèn)導(dǎo)出并非如此:
export default foo;
使用這種語(yǔ)法時(shí),導(dǎo)出的不是變量,而是變量值??梢韵襁@樣導(dǎo)出默認(rèn)值,而無(wú)需使用變量:
export default ‘hello’;
export default 42;
如果查看測(cè)驗(yàn) #1 中使用導(dǎo)出語(yǔ)法的表格,就會(huì)發(fā)現(xiàn) export default function () {} 與 export default foo ( Export of values ) 所處的列 ( Default export ) 不同。
這是因?yàn)樗鼈兊男袨榉绞讲煌?,函?shù)仍然作為活引用傳遞:
// module.mjs
export { foo };
export default function foo() {};
setTimeout(() => {
foo = 'baz';
}, 1000);
// index.mjs
import defaultFoo, { foo } from './module.mjs';
setTimeout(() => {
console.log(foo); // baz
console.log(defaultFoo); //baz
}, 2000);
export { foo as default }; 位于 Named Export 列,與這兩列都不同。但對(duì)我們來(lái)說(shuō),唯一重要的是它不在 Export of values 列中。因此,這意味著當(dāng)以這種方式導(dǎo)出數(shù)據(jù)時(shí),它將與導(dǎo)入值進(jìn)行實(shí)時(shí)綁定。
測(cè)驗(yàn) #8: 40% 的正確答案
// index.mjs
import { shouldLoad } from './module1.mjs';
let num = 0;
if (shouldLoad) {
import { num } from './module2.mjs';
}
console.log(num);
// module1.mjs
export const shouldLoad = true;
//module2.mjs
export const num = 1;
import { num } from ‘./module2.mjs’; 行將會(huì)出錯(cuò),因?yàn)閷?dǎo)入結(jié)構(gòu)必須位于腳本的頂層:
SyntaxError: Unexpected token ‘{‘
這是一個(gè)重要的限制,加上在文件路徑中使用變量的限制,使得 ES6 模塊成為靜態(tài)模塊。這意味著,與 Node.js 中使用的 Common.js 模塊不同,不必執(zhí)行代碼就能找出模塊之間的所有依賴關(guān)系。
在這個(gè)使用 Common.js 模塊的示例中,要確定將加載 a 或 b 模塊,需要運(yùn)行以下代碼:
let result;
if (foo()) {
result = require('a');
} else {
result = require('b');
}
模塊的靜態(tài)特性有很多好處。以下是其中一些:
- 總是知道導(dǎo)入數(shù)據(jù)的確切結(jié)構(gòu)。這有助于在執(zhí)行代碼前發(fā)現(xiàn)錯(cuò)別字。
- 異步加載。這是因?yàn)槟K是靜態(tài)的,可以在執(zhí)行模塊主體之前加載導(dǎo)入。
- 支持循環(huán)依賴關(guān)系。我們將在下一次測(cè)驗(yàn)中詳細(xì)探討這種可能性。
- 高效捆綁。在此不多贅述,您可以在本文中自行了解 Rollup 捆綁程序如何有效地構(gòu)建 ES6 模塊。
測(cè)驗(yàn) #9: 3% 的正確答案
// index.mjs
import { double, square } from './module.mjs';
export function calculate(value) {
return value % 2 ? square(value) : double(value);
}
// module.mjs
import { calculate } from './index.mjs';
export function double(num) {
return num * 2;
}
export function square(num) {
return num * num;
}
console.log(calculate(3));
在上面的代碼中,我們可以看到循環(huán)依賴關(guān)系: index.mjs 從 module.mjs 導(dǎo)入 double 和 square 函數(shù),而 module.mjs 從 index.mjs 導(dǎo)入 calculation 函數(shù)。
這段代碼之所以能運(yùn)行,是因?yàn)?ES6 模塊本質(zhì)上非常支持循環(huán)依賴關(guān)系。例如,如果我們將這段代碼改寫為使用 Common.js 模塊,它將不再工作:
// index.js
const helpers = require('./module.js');
function calculate(value) {
return value % 2 ? helpers.square(value) : helpers.double(value);
}
module.exports = {
calculate
}
// module.js
const actions = require('./index.js');
function double(num) {
return num * 2;
}
function square(num) {
return num * num;
}
console.log(actions.calculate(3)); // TypeError: actions.calculate is not a function
module.exports = {
double,
square
}
- index.js 開(kāi)始加載。
- 加載在第一行中斷,以加載 module.js :const helpers = require(‘./module.js’)。
- module.js 開(kāi)始加載。
- 在 console.log(actions.calculate(3)); 行中,由于 actions.calculate 未定義,代碼出錯(cuò)。這是因?yàn)?Common.js 同步加載模塊。 index.js 尚未加載,其導(dǎo)出對(duì)象目前為空。
如果調(diào)用一個(gè)帶延遲的導(dǎo)入函數(shù), index.js 模塊將有時(shí)間加載,代碼也將相應(yīng)地工作:
// module.js
const actions = require('./index.js');
function double(num) {
return num * 2;
}
function square(num) {
return num * num;
}
function someFunctionToCallLater() {
console.log(actions.calculate(3)); // Works
}
module.exports = {
double,
square
}
從前面的測(cè)驗(yàn)中了解到的,ES6 模塊支持循環(huán)依賴關(guān)系,因?yàn)樗鼈兪庆o態(tài)的--模塊的依賴關(guān)系在代碼執(zhí)行之前就已加載。
使上述代碼工作的另一個(gè)因素是提升。當(dāng)調(diào)用 calculate 函數(shù)時(shí),我們還沒(méi)有進(jìn)入定義該函數(shù)的行。
下面是捆綁模塊后的代碼:
function double(num) {
return num * 2;
}
function square(num) {
return num * num;
}
console.log(calculate(3));
function calculate(value) {
return value % 2 ? square(value) : double(value);
}
如果沒(méi)有變量提升,它將無(wú)法工作。
如果我們將計(jì)算聲明函數(shù)改為函數(shù)表達(dá)式:
export let calculate = function(value) {
return value % 2 ? square(value) : double(value);
}
會(huì)出現(xiàn)以下錯(cuò)誤:
ReferenceError: Cannot access ‘calculate’ before initialization
測(cè)驗(yàn) #10: 31%的正確答案
// index.mjs
import { num } from './module.mjs';
console.log(num);
export let num = 0;
num = await new Promise((resolve) => {
setTimeout(() => resolve(1), 1000);
});
頂層 await 是一個(gè)非常有用的特性,許多開(kāi)發(fā)者都不了解,也許是因?yàn)樗窃谧罱?ECMAScript 2022 中才引入的。啊,真不錯(cuò)!
頂層 await 使模塊能夠像大型異步函數(shù)一樣運(yùn)作:通過(guò)頂層 await,ECMAScript 模塊(ESM)可以等待資源,導(dǎo)致導(dǎo)入它們的其他模塊在開(kāi)始評(píng)估其主體之前必須等待。
模塊的標(biāo)準(zhǔn)行為是,在加載模塊導(dǎo)入的所有模塊并執(zhí)行其代碼之前,模塊中的代碼不會(huì)被執(zhí)行(參見(jiàn)測(cè)驗(yàn) #2)。事實(shí)上,隨著頂級(jí)等待的出現(xiàn),一切都沒(méi)有改變。模塊中的代碼不會(huì)被執(zhí)行,直到所有導(dǎo)入模塊中的代碼都被執(zhí)行,只是現(xiàn)在這包括等待模塊中所有等待的承諾被解決。
// index.js
console.log('index.js');
import { num } from './module.js';
console.log('num = ', num);
// module.js
export let num = 5;
console.log('module.js');
await new Promise((resolve) => {
setTimeout(() => {
console.log('module.js: promise 1');
num = 10;
resolve();
}, 1000);
});
await new Promise((resolve) => {
setTimeout(() => {
console.log('module.js: promise 2');
num = 20;
resolve();
}, 2000);
});
輸出:
module.js
module.js: promise 1
module.js: promise 2
index.js
num = 20
如果我們刪除 module.js 中第 5 行和第 13 行的等待,并在文件 index.js 中添加超時(shí),就會(huì)像這樣:
console.log('index.js');
import { num } from './module.js';
console.log('num = ', num);
setTimeout(() => {
console.log('timeout num = ', num);
}, 1000);
setTimeout(() => {
console.log('timeout num = ', num);
}, 2000);
輸出:
module.js
index.js
num = 5
module.js: promise 1
timeout num = 10
module.js: promise 2
timeout num = 20
我們將在今后的測(cè)驗(yàn)中再次使用頂級(jí)等待功能。
測(cè)驗(yàn) #11: 16%的正確答案
//index.mjs
import { shouldLoad } from './module1.mjs';
let num = 0;
if (shouldLoad) {
({ num } = import('./module2.mjs'));
}
console.log(num);
// module1.mjs
export const shouldLoad = true;
//module2.mjs
export const num = 1;
import() 調(diào)用(通常稱為動(dòng)態(tài)導(dǎo)入)是一種類似函數(shù)的表達(dá)式,它允許異步動(dòng)態(tài)加載 ECMAScript 模塊。它允許繞過(guò)導(dǎo)入聲明的語(yǔ)法限制,有條件或按需加載模塊。
該功能在 ES2020 中引入。
import(module) 返回一個(gè) promise ,該承諾會(huì)履行到一個(gè)包含模塊所有輸出的對(duì)象。由于 import(module) 返回的是一個(gè) promise,為了修正測(cè)驗(yàn)代碼,我們必須在導(dǎo)入調(diào)用之前添加 await 關(guān)鍵字:
if (shouldLoad) {
({ num } = await import('./module2.mjs'));
}
在這里,我們?cè)俅问褂庙攲?nbsp;await,這讓我們想起了這一功能的酷炫之處。
我敢肯定,你的應(yīng)用程序至少有一次出錯(cuò)崩潰了:
SyntaxError: await is only valid in async functions
當(dāng)試圖從全局作用域調(diào)用異步函數(shù)時(shí),經(jīng)常會(huì)出現(xiàn)這種情況。為了解決這個(gè)問(wèn)題,我們必須躲避丑陋的代碼:
(async () => {
await [someAsyncFunc]();
})();
這不僅難看,而且在使用此模式異步加載模塊時(shí)可能會(huì)導(dǎo)致錯(cuò)誤。例如
// module1.mjs
let num;
(async () => {
({ num } = await import(‘./module2.mjs’));
})();
export { num };
// module2.mjs
export const num = 5;
導(dǎo)入 module1.mjs 時(shí), num 的結(jié)果會(huì)是什么 - 來(lái)自 module2 或 undefined 的值?這取決于何時(shí)訪問(wèn)變量:
import { num } from './module1.mjs';
console.log(num); // undefined
setTimeout(() => console.log(num), 100); // 5
有了頂級(jí) await ,只要您訪問(wèn)從 module1 導(dǎo)入的 num ,它就永遠(yuǎn)不會(huì)是 undefined :
let { num } = await import('./module2.mjs');
export { num };
import { num } from './module1.mjs';
console.log(num); // 5
測(cè)驗(yàn) #12: 21% 的正確答案
// index.mjs
const module1 = await import('./module1.mjs');
const module2 = await import('./module2.mjs');
console.log(module1, module2);
function multiply(num1, num2) { return num1 * num2; }
console.log(multiply(module1, module2));
// module1.mjs
export default await new Promise((resolve) => resolve(1));
// module2.mjs
export default await new Promise((resolve) => resolve(2));
上述代碼會(huì)出錯(cuò):
TypeError: Cannot convert object to primitive value
同意,一個(gè)相當(dāng)意外的錯(cuò)誤措辭。讓我們來(lái)看看這個(gè)錯(cuò)誤從何而來(lái)。
在這段代碼中,我們使用了動(dòng)態(tài)導(dǎo)入,這在前面的示例中已經(jīng)介紹過(guò)。要理解這段代碼中的問(wèn)題,我們需要仔細(xì)看看 import() 的返回值。
變量 module1 和 module2 的值與我們的預(yù)期不同。 import() 返回一個(gè) promise ,該promise 將實(shí)現(xiàn)一個(gè)與命名空間導(dǎo)入形狀相同的對(duì)象:
import * as name from moduleName
default 輸出可作為名為 default 的鍵使用。
因此,在變量 module1 和 module2 中分別有對(duì)象 { default: 1 } 和 { default: 2 } ,而不是值 1 和 2 。
那么,為什么兩個(gè)對(duì)象相乘時(shí)會(huì)出現(xiàn)如此奇怪的錯(cuò)誤,而不是我們習(xí)慣的 NaN 呢?
這是因?yàn)榉祷氐膶?duì)象具有 null 原型。因此,它沒(méi)有用于將對(duì)象轉(zhuǎn)換為基元的 toString() 方法。如果這個(gè)對(duì)象有一個(gè) Object 原型,我們就會(huì)在控制臺(tái)中看到 NaN 。
要修復(fù)測(cè)驗(yàn)代碼,我們需要做以下更改:
console.log(pow(module1.default, module2.default));
或:
const { default: module1 } = await import('./module1.mjs');
const { default: module2 } = await import('./module2.mjs');
測(cè)驗(yàn) #13:17%的正確答案
// index.js
import * as values from './intermediate.js';
console.log(values.x);
// module1.js
export const x = 1;
// module2.js
export const x = 2;
// intermediate.js
export * from './module1.js';
export * from './module2.js';
export * from ‘module’ 語(yǔ)法會(huì)將 "模塊"文件中所有已命名的導(dǎo)出內(nèi)容重新導(dǎo)出為當(dāng)前文件中已命名的導(dǎo)出內(nèi)容。如果存在多個(gè)同名導(dǎo)出,則不會(huì)重新導(dǎo)出其中任何一個(gè)。
因此,運(yùn)行這段代碼時(shí),我們會(huì)在控制臺(tái)中看到 undefined 。只有 17% 的答題者回答正確,大多數(shù)答題者(59%)認(rèn)為這段代碼會(huì)出錯(cuò)。事實(shí)上,這種無(wú)聲的失敗似乎并不是嚴(yán)格模式的典型表現(xiàn)。(如果您知道這種行為的原因,請(qǐng)?jiān)谠u(píng)論中告訴我。
順便提一下,如果在同樣的情況下顯式導(dǎo)入 x ,就會(huì)出現(xiàn)預(yù)期的錯(cuò)誤:
import { x } from ‘./intermediate.js’;
SyntaxError: The requested module ‘./intermediate.js’ contains conflicting star exports for name ‘x’
最后
沖~~~