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