// index.mjsimport { default } from './module.mjs';console.log(default);
// module.mjsexport default 'bar';
首先,讓我們記住各種導入和導出語法:
如果檢查表中的 Import 語法,就會發現沒有與我們的代碼相匹配的語法:
import { default } from ‘./module.mjs’;
因為禁止使用這種語法。測驗代碼會出現以下錯誤:
SyntaxError: Unexpected reserved word
在 import { default } from ‘./module.mjs’; 行中, default 是 export 的名稱,也是該作用域中的變量名稱,這是被禁止的,因為 default 是一個保留字。解決方法很簡單:
import { default as foo } from ‘./module.mjs’;
現在, default 是導出的名稱, foo 是變量的名稱。換句話說,如果你想在默認導出中使用命名導入語法,就必須重命名它。就是這樣,非常簡單!
// index.jsconsole.log('index.js');import { sum } from './helper.js';console.log(sum(1, 2));
// helper.jsconsole.log('helper.js');export const sum = (x, y) => x + y;
沒有多少開發人員知道的一個重要的細微差別是,導入是被提升的。也就是說,在引擎解析代碼時,導入就會被加載。所有依賴項都將在代碼運行前加載。
因此,我們將按照以下順序查看日志:
helper.js, index.js, 3
如果希望在導入聲明之前執行某些代碼,可考慮將其移至單獨的文件中:
// new index.jsimport './logs.js';import { sum } from './helper.js';console.log(sum(1, 2));
console.log('index.js');
現在我們有了預期的輸出結果:
index.js, helper.js, 3
// index.mjsimport './module.mjs';import { num } from './counter.mjs';console.log('index num =', num);
// module.mjsimport { num } from './counter.mjs';console.log('module num =', num);
// counter.mjsif (!globalThis.num) { globalThis.num = 0;}export const num = ++globalThis.num;
Modules are singletons. 模塊是單例。
無論從同一位置或不同位置導入模塊多少次,模塊都只會被執行和加載一次。換句話說,模塊實例只有一個。
// index.mjsimport './module.mjs?param=5;'
// module.mjsconsole.log(import.meta.url);
這個問題如果沒有對ES6有比較深的理解,就不太好回答出來。
根據 MDN:
import.meta 對象為 JavaScript 模塊提供特定于上下文的元數據。它包含有關模塊的信息。
它返回一個帶有 url 屬性的對象,url 屬性表示模塊的基本 URL。對于外部腳本,url 將是獲取腳本的 URL;對于內嵌腳本,url 將是包含腳本的文檔的基本 URL。
請注意,這將包括查詢參數和/或哈希值(即,跟在 "?" 或 "#" 之后的部分)。
import myCounter from './counter';myCounter += 1;console.log(myCounter);
// counter.jslet counter = 5;export default counter;
另一個大多數開發者容易忽視的非常重要的點是,在導入模塊的作用域中,導入的變量表現得像常量。
為了使代碼正常工作,我們可以導出一個對象,例如,并更改其屬性。
// index.mjsimport foo from './module.mjs';console.log(typeof foo);
// module.mjsfoo = 25;export default function foo() {}
首先,這:
export default function foo() {}
等于:
function foo() {}export { foo as default }
這也等于:
function foo() {}export default foo
現在是時候回想起函數是如何被提升的,以及變量的初始化總是在函數/變量聲明之后進行。
引擎處理完模塊代碼后,看起來是這樣的:
function foo() {}foo = 25;export { foo as default }
因此,測驗結果就是 number 。
// index.mjsimport defaultFoo, { foo } from './module.mjs';setTimeout(() => { console.log(foo); console.log(defaultFoo);}, 2000);
// module.mjslet foo = 'bar';export { foo };export default foo;setTimeout(() => { foo = 'baz';}, 1000);
在大多數情況下,導入的數據是實時的。也就是說,如果導出的值發生了變化,這種變化會反映在導入的變量上。
但默認導出并非如此:
export default foo;
使用這種語法時,導出的不是變量,而是變量值??梢韵襁@樣導出默認值,而無需使用變量:
export default ‘hello’;export default 42;
如果查看測驗 #1 中使用導出語法的表格,就會發現 export default function () {} 與 export default foo ( Export of values ) 所處的列 ( Default export ) 不同。
這是因為它們的行為方式不同,函數仍然作為活引用傳遞:
// 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 列中。因此,這意味著當以這種方式導出數據時,它將與導入值進行實時綁定。
// index.mjsimport { shouldLoad } from './module1.mjs';let num = 0;if (shouldLoad) { import { num } from './module2.mjs';}console.log(num);
// module1.mjsexport const shouldLoad = true;
//module2.mjsexport const num = 1;
import { num } from ‘./module2.mjs’; 行將會出錯,因為導入結構必須位于腳本的頂層:
SyntaxError: Unexpected token ‘{‘
這是一個重要的限制,加上在文件路徑中使用變量的限制,使得 ES6 模塊成為靜態模塊。這意味著,與 Node.js 中使用的 Common.js 模塊不同,不必執行代碼就能找出模塊之間的所有依賴關系。
在這個使用 Common.js 模塊的示例中,要確定將加載 a 或 b 模塊,需要運行以下代碼:
let result;if (foo()) { result = require('a');} else { result = require('b');}
模塊的靜態特性有很多好處。以下是其中一些:
// index.mjsimport { double, square } from './module.mjs';export function calculate(value) { return value % 2 ? square(value) : double(value);}
// module.mjsimport { calculate } from './index.mjs';export function double(num) { return num * 2;}export function square(num) { return num * num;}console.log(calculate(3));
在上面的代碼中,我們可以看到循環依賴關系: index.mjs 從 module.mjs 導入 double 和 square 函數,而 module.mjs 從 index.mjs 導入 calculation 函數。
這段代碼之所以能運行,是因為 ES6 模塊本質上非常支持循環依賴關系。例如,如果我們將這段代碼改寫為使用 Common.js 模塊,它將不再工作:
// index.jsconst helpers = require('./module.js');function calculate(value) { return value % 2 ? helpers.square(value) : helpers.double(value);}module.exports = { calculate}
// module.jsconst 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 functionmodule.exports = { double, square}
如果調用一個帶延遲的導入函數, index.js 模塊將有時間加載,代碼也將相應地工作:
// module.jsconst 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 模塊支持循環依賴關系,因為它們是靜態的--模塊的依賴關系在代碼執行之前就已加載。
使上述代碼工作的另一個因素是提升。當調用 calculate 函數時,我們還沒有進入定義該函數的行。
下面是捆綁模塊后的代碼:
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);}
如果沒有變量提升,它將無法工作。
如果我們將計算聲明函數改為函數表達式:
export let calculate = function(value) { return value % 2 ? square(value) : double(value);}
會出現以下錯誤:
ReferenceError: Cannot access ‘calculate’ before initialization
// index.mjsimport { num } from './module.mjs';console.log(num);
export let num = 0;num = await new Promise((resolve) => { setTimeout(() => resolve(1), 1000);});
頂層 await 是一個非常有用的特性,許多開發者都不了解,也許是因為它是在最近的 ECMAScript 2022 中才引入的。啊,真不錯!
頂層 await 使模塊能夠像大型異步函數一樣運作:通過頂層 await,ECMAScript 模塊(ESM)可以等待資源,導致導入它們的其他模塊在開始評估其主體之前必須等待。
模塊的標準行為是,在加載模塊導入的所有模塊并執行其代碼之前,模塊中的代碼不會被執行(參見測驗 #2)。事實上,隨著頂級等待的出現,一切都沒有改變。模塊中的代碼不會被執行,直到所有導入模塊中的代碼都被執行,只是現在這包括等待模塊中所有等待的承諾被解決。
// index.jsconsole.log('index.js');import { num } from './module.js';console.log('num = ', num);
// module.jsexport 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.jsmodule.js: promise 1module.js: promise 2index.jsnum = 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.jsindex.jsnum = 5module.js: promise 1timeout num = 10module.js: promise 2timeout num = 20
我們將在今后的測驗中再次使用頂級等待功能。
//index.mjsimport { shouldLoad } from './module1.mjs';let num = 0;if (shouldLoad) { ({ num } = import('./module2.mjs'));}console.log(num);
// module1.mjsexport const shouldLoad = true;
//module2.mjsexport const num = 1;
import() 調用(通常稱為動態導入)是一種類似函數的表達式,它允許異步動態加載 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
當試圖從全局作用域調用異步函數時,經常會出現這種情況。為了解決這個問題,我們必須躲避丑陋的代碼:
(async () => { await [someAsyncFunc]();})();
這不僅難看,而且在使用此模式異步加載模塊時可能會導致錯誤。例如
// module1.mjslet num;(async () => { ({ num } = await import(‘./module2.mjs’));})();export { num };
// module2.mjsexport const num = 5;
導入 module1.mjs 時, num 的結果會是什么 - 來自 module2 或 undefined 的值?這取決于何時訪問變量:
import { num } from './module1.mjs';console.log(num); // undefinedsetTimeout(() => 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
// index.mjsconst 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.mjsexport default await new Promise((resolve) => resolve(1));
// module2.mjsexport default await new Promise((resolve) => resolve(2));
上述代碼會出錯:
TypeError: Cannot convert object to primitive value
同意,一個相當意外的錯誤措辭。讓我們來看看這個錯誤從何而來。
在這段代碼中,我們使用了動態導入,這在前面的示例中已經介紹過。要理解這段代碼中的問題,我們需要仔細看看 import() 的返回值。
變量 module1 和 module2 的值與我們的預期不同。 import() 返回一個 promise ,該promise 將實現一個與命名空間導入形狀相同的對象:
import * as name from moduleName
default 輸出可作為名為 default 的鍵使用。
因此,在變量 module1 和 module2 中分別有對象 { default: 1 } 和 { default: 2 } ,而不是值 1 和 2 。
那么,為什么兩個對象相乘時會出現如此奇怪的錯誤,而不是我們習慣的 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');
// index.jsimport * as values from './intermediate.js';console.log(values.x);
// module1.jsexport const x = 1;
// module2.jsexport const x = 2;
// intermediate.jsexport * from './module1.js';export * from './module2.js';
export * from ‘module’ 語法會將 "模塊"文件中所有已命名的導出內容重新導出為當前文件中已命名的導出內容。如果存在多個同名導出,則不會重新導出其中任何一個。
因此,運行這段代碼時,我們會在控制臺中看到 undefined 。只有 17% 的答題者回答正確,大多數答題者(59%)認為這段代碼會出錯。事實上,這種無聲的失敗似乎并不是嚴格模式的典型表現。(如果您知道這種行為的原因,請在評論中告訴我。
順便提一下,如果在同樣的情況下顯式導入 x ,就會出現預期的錯誤:
import { x } from ‘./intermediate.js’;
SyntaxError: The requested module ‘./intermediate.js’ contains conflicting star exports for name ‘x’
沖~~~
本文鏈接:http://www.tebozhan.com/showinfo-26-11885-0.html50 種 ES6 模塊,面試被問麻了
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
下一篇: Havoc遠控源碼剖析(協議篇)