ECMAScript?modules規(guī)范示例詳解
引言
很多編程語言都有模塊這一概念,JavaScript 也不例外,但在 ECMAScript 2015 規(guī)范發(fā)布之前,JavaScript 沒有語言層面的模塊語法。模塊實際上是一種代碼重用機制,要實現(xiàn)代碼重用,將不同的功能劃分到不同的文件中是必不可少的,如何在其他的文件中使用這些文件定義的功能呢?在 ECMAScript 2015 之前,web 開發(fā)人員不得不尋求 JavaScript 語法之外的解決方法,例如:SystemJS、RequireJS 等模塊加載工具,也有開發(fā)人員使用 webpack、Browserify 等模塊打包工具。ECMAScript 2015 發(fā)布之后,JavaScript 擁有了語言層面的模塊語法,它被稱為 ECMAScript modules,簡稱 ES modules,這使 web 開發(fā)人員很容易就能創(chuàng)建模塊,使用模塊。
在本文中會介紹 ES modules 的基本用法、ES modules 的特點以及在瀏覽器中使用 ES modules。
基本語法
ES modules 是 JavaScript 的標準模塊系統(tǒng),模塊是一個簡單的 JavaScript 文件,在這個文件中包含 export 或者 import 關(guān)鍵字。export 用于將模塊中聲明的內(nèi)容導(dǎo)出,import 用于從其他模塊中導(dǎo)入。
模塊導(dǎo)出的4種寫法
模塊導(dǎo)出用到的關(guān)鍵字是 export,它只能在模塊頂層使用。模塊可以導(dǎo)出函數(shù)、類、或其他基本類型等。模塊導(dǎo)出有4種寫法
- 默認導(dǎo)出
export default function myFunc() {}
export default function () {}
export default class MyClass {}
export { foo as default }
export default 'Hello Es modules!'
- 行內(nèi)命名導(dǎo)出
export function myFunc() {}
export class MyClass {}
export const fooStr = 'Hello Es modules!'
- 通過一個 export 子句批量命名導(dǎo)出
function myFunc() {}
class MyClass {}
const fooStr = 'Hello Es modules!'
export {myFunc, MyClass , fooStr } // 在這個地方一次性導(dǎo)出多個
- 重新導(dǎo)出
// 重新導(dǎo)出 other_module 中除默認導(dǎo)出之外的內(nèi)容
export * from './other_module.js'
// 重新導(dǎo)出 other_module 中的默認導(dǎo)出
export { default } from './other_module.js'
// 重新導(dǎo)出 other_module 中的默認導(dǎo)出,并且將 other_module 中的 sayName 命名為 getName 之后再導(dǎo)出
export { default, sayName as getName } from './other_module.js'
雖然模塊導(dǎo)出有4種寫法,但是只有兩種方式,一種默認導(dǎo)出,另一種是命名導(dǎo)出,在同一個模塊中命名導(dǎo)出可以有多個,默認導(dǎo)出只能有一個,這兩種方式可以混合使用。
在軟件開發(fā)的過程中,通常有多種寫法能到達同一目的,但并不是每一種寫法都值得推薦,模塊導(dǎo)出也是類似的。如果在同一個模塊中,即有默認導(dǎo)出,又有行內(nèi)命名導(dǎo)出,還有 export 子句批量命名導(dǎo)出,那么你的模塊很可能會變得混亂。在這里我推薦使用默認導(dǎo)出,并且將 export default 放在模塊的末尾。如果你必須要命名導(dǎo)出,我推薦使用export 子句批量命名導(dǎo)出,并將 export 子句放在文件的末尾。
3中模塊說明符
介紹完模塊導(dǎo)出之后,按理說應(yīng)該介紹模塊導(dǎo)入,但我決定先介紹模塊說明符,這是因為模塊導(dǎo)入依賴模塊說明符。說明符是字符串字面值,它表示導(dǎo)入模塊的路徑,說明符一共有三種類型,分別是:相對路徑、絕對路徑和 bare(裸 露) 模式。
- 相對路徑
import foo from './myModule.js'
import { sayName } from '../other_module.js'
相對路徑說明符以 / 、./ 、../ 開頭,當使用相對路徑說明符時不能省略文件的擴展名。在 web 項目開發(fā)中使用相對路徑導(dǎo)入模塊的時候,你可能省略了文件擴展名,它還是能夠工作,那是因為你的項目使用了如 webpack 這樣的模塊打包工具。
- 絕對路徑
import React from 'https://cdn.skypack.dev/react'
上述代碼表示從 cdn 導(dǎo)入模塊,當使用絕對路徑導(dǎo)入模塊時,是否能省略文件擴展名,這與服務(wù)器配置相關(guān)。
- bare(裸 露) 模式
import React from 'react' import Foo from 'react/lib.js'
bare 模式從 node_module 中導(dǎo)入模塊,在 web 項目開發(fā)中,用這種說明符導(dǎo)入模塊很常見,但是 ES modules 并不支持它,在你的項目中,你之所以能夠使用它,是因為你的項目用了如 webpack 這樣的模塊打包工具。
到目前為止,我已經(jīng)介紹完了3種模塊說明符,ES modules 只支持其中兩種,分別是:相對路徑和絕對路徑。
模塊導(dǎo)入的 6 寫法
模塊導(dǎo)入用到的關(guān)鍵字是 import,import 與 export 一樣只能在模塊頂部使用,模塊說明符不能包含變量,它必須是固定的字符串字面量。模塊導(dǎo)入有6種不同的寫法,如下:
- 默認導(dǎo)入
// 你可以將 myFunc 改成任何你喜歡的變量名 import myFunc from './myModule.js'
- 將模塊作為一個對象導(dǎo)入(即命名空間導(dǎo)入)
import * as api from './myModule.js' // 通過對象的 default 屬性訪問 myModule.js 中的默認導(dǎo)出 console.log(api.default)
- 命名導(dǎo)入
// 導(dǎo)入 myModule.js 中的fooStr
import { fooStr } from './myModule.js'
// 將myModule.js中默認導(dǎo)出命名為myFunc
import { default as myFunc } './myModule.js'
// 將 myModule.js中的 fooStr 命名為 myStr
import { fooStr as myStr } from './myModule.js'
當某個模塊中導(dǎo)出了很多內(nèi)容,而你只需要用到它導(dǎo)出的一部分內(nèi)容,你可以使用這個寫法只導(dǎo)入你需要的部分,在做搖樹優(yōu)化的時候這至關(guān)重要。
- 只加載模塊,不導(dǎo)入任何東西
import './myModule.js'
不會將 myModule.js 中的任何內(nèi)容導(dǎo)入到當前模塊,但是會執(zhí)行 myModule.js 模塊體,這通常用于執(zhí)行一些初始化操作。
- 將默認導(dǎo)入與命名導(dǎo)入混合使用
import myFunc, { fooStr } from './myModule.js'
- 將默認導(dǎo)入與命名空間導(dǎo)入混合使用
import myFunc, * as api from './myModule.js'
補充:同一個模塊可以被多次導(dǎo)入,但是它的模塊體只會執(zhí)行一次
ES modules的 4 個特點
導(dǎo)入是導(dǎo)出的只讀引用
例如有個模塊 A,它導(dǎo)出了一個變量 count,模塊 B 導(dǎo)入模塊 A 的 count,count 對模塊 B 而言是只讀的,所以在模塊 B 中不能直接修改 count,下面用代碼演示一下:
// 模塊A的代碼如下:
export var count = 0 // 注意:這里用的是 var 關(guān)鍵字
// 模塊B的代碼如下:
import { count } from './moduleA.js'
count++ // Uncaught TypeError: Assignment to constant variable
將上述代碼放在瀏覽器中運行,瀏覽器會報錯,錯誤類型是:TypeError。如果模塊 A 導(dǎo)出了對象 obj,在模塊 B 中不能直接給 obj 賦值,但是可以增、刪、改 obj 中的屬性。
現(xiàn)在我已經(jīng)介紹了只讀的含義,下面介紹引用的含義。引用意味著在項目中多個模塊用的是同一個變量,例如:模塊 B 和模塊 C 都導(dǎo)入了模塊 A 的 count 和 changeCount 函數(shù),模塊 B 通過 changeCount 修改了 count 的值,模塊C中的 count 會被一同修改,代碼如下:
// 模塊A的代碼如下:
export var count = 0
export function changeCount() {
count++
}
// 模塊B的代碼如下:
import { count, changeCount } from './moduleA.js'
changeCount ()
console.log(count) // 1
// 模塊C的代碼如下:
import { count } from './moduleA.js'
console.log(count) // 1
模塊 B 和模塊 C 導(dǎo)入的是引用,而非副本,模塊導(dǎo)出的變量在整個項目中是一個單例。
支持循環(huán)依賴
循環(huán)依賴指的是兩個模塊相互依賴,比如模塊 A 導(dǎo)入了模塊 B,模塊 B 又導(dǎo)入了模塊 A。盡管 ES modules 支持循環(huán)依賴,但應(yīng)該避免,因為這會使兩個模塊強耦合。ES modules 支持循環(huán)依賴這是因為導(dǎo)入是導(dǎo)出的只讀引用。
導(dǎo)入會被提升
如果你知道 JavaScript 函數(shù)提升,那么你很容易理解 ES modules 的導(dǎo)入提升。由于 ES modules 的導(dǎo)入會被提升到模塊作用域的開頭,所以你不需要先導(dǎo)入再使用。下面的代碼可以工作:
foo() import foo from './myModule.js'
導(dǎo)出和靜態(tài)導(dǎo)入必須位于模塊的頂層
導(dǎo)出必須位于模塊的頂層這一點毋庸置疑,在 ECMAScript 2020 規(guī)范中添加了動態(tài)導(dǎo)入,它使模塊導(dǎo)入可以不必位于模塊的頂層。在后面會單獨介紹動態(tài)導(dǎo)入,在這里介紹的是靜態(tài)導(dǎo)入。
ECMAScript 2020 之前,JavaScript 的 ES modules 是一個靜態(tài)模塊系統(tǒng),它意味著模塊的依賴項在你寫代碼的時候就確定了,不用等到代碼運行階段才確定,這讓代碼打包工具,如 webpack,很容易就能分析出 ES 模塊中的依賴,給搖樹優(yōu)化提供了便利。
即便 ECMAScript 2020 增加了動態(tài)導(dǎo)入,靜態(tài)導(dǎo)入與動態(tài)導(dǎo)入在寫法上有差異,靜態(tài)導(dǎo)入使用 import 關(guān)鍵字,動態(tài)導(dǎo)入使用 import()。靜態(tài)導(dǎo)入只能位于模塊頂層。
模塊與常規(guī)JavaScript腳本的差異
- 模塊運行在嚴格模式下
- 模塊具備詞法頂部作用域
這句話的意思是,在模塊中創(chuàng)建的變量,如:foo,不能通過 window.foo 訪問。代碼如下:
var foo = 'hi'
console.log(window.foo) // undefined
console.log(foo) // hi
export {} // 將這個文件標記成模塊
在模塊中的聲明的變量是針對該模塊的,這意味著在模塊中聲明的任何變量對其他模塊都不可用,除非它們被顯式地導(dǎo)出。
- 模塊中的 this 關(guān)鍵字沒有指向全局 this,它是 undefined,如果要在模塊中訪問全局 this 要使用 globalThis,在瀏覽器中 globalThis 是 window 對象。
- export 和靜態(tài)導(dǎo)入 import 只能在模塊中使用
- 在模塊頂層能使用 await 關(guān)鍵字,在常規(guī) JavaScript 腳本中只能在 async 函數(shù)中使用 await 關(guān)鍵字
注意:由于 JavaScript 運行時會區(qū)別對待模塊和常規(guī)的 JavaScript 腳本,所以在寫代碼的時候做好顯示地標記 JavaScript 文件是模塊,只要 JavaScript 文件中包含 export 或者 import 關(guān)鍵字,JavaScript 運行時就會認為這個文件是模塊
在這部分介紹的這 5 個差異是與 JavaScript 運行環(huán)境無關(guān)的差異,在之后的部分會介紹在瀏覽器中使用 ES modules,這那里會補充一些新的差異。
在瀏覽器中使用 ES modules
現(xiàn)代瀏覽器支持 ES modules,你可以將 script 標簽的 type 屬性設(shè)置為 module 來告訴瀏覽器這個腳本是模塊,代碼如下:
<!--外部模塊-->
<script type="module" src="./module.js"></script>
<!--內(nèi)聯(lián)模塊-->
<script type="module">
import {count} from './moduleA.js';
import React from 'https://cdn.skypack.dev/react'
console.log(count, React)
</script>
出于對兼容性的考慮,可能還需要 <script nomodule src=’xxx.js’></script>,在這里不做介紹。
在之前介紹了模塊和常規(guī) JavaScript 腳本與運行環(huán)境無關(guān)的差異,現(xiàn)在來介紹在瀏覽器環(huán)境中二者的差異
- 模塊只會被執(zhí)行一次
不管模塊被引入了多少次,它只會被執(zhí)行一次,而常規(guī)的 JavaScript 腳本執(zhí)行次數(shù)與它被添加到 DOM 的次數(shù)一致,添加多少次就執(zhí)行多少次。比如有下面一段代碼:
<!--外部模塊-->
<script type="module" src="./module.js"></script>
<script type="module" src="./module.js"></script>
<script type="module" src="./module.js"></script>
<!--內(nèi)聯(lián)模塊-->
<script type="module">
import { count } from './module.js';
</script>
<script src='./classic.js'></script>
<script src='./classic.js'></script>
在上述代碼中 module.js 只會被執(zhí)行一次,classic.js 會被執(zhí)行兩次
- 下載模塊腳本不會阻塞 HTML 解析
默認情況,當瀏覽器下載常規(guī)外部腳本時,它會暫停解析 HTML,我們可以在 script 標簽上添加 defer 屬性,使瀏覽器在下載腳本期間不暫停解析 HTML。當下載模塊腳本時,瀏覽器默認模塊腳本是 defer 的。
下圖展示了瀏覽器獲取外部模塊腳本和常規(guī)腳本的流程

上圖源于 (html.spec.whatwg.org/multipage/s…)
- 模塊腳本通過 CORS 獲取
模塊腳本以及它的依賴項是通過 CORS 獲取的,當獲取一個跨域的模塊腳本時需要特別注意這個問題,跨域腳本的響應(yīng)頭 Access-Control-Allow-Origin 必須包含當前域,否則模塊會獲取失敗,而獲取常規(guī)腳本則沒有這個限制。
為了保證獲取同源模塊腳本時,瀏覽器始終帶上 credentials(cookie 等),推薦給 script 標簽加上 crossorigin 屬性。
動態(tài)導(dǎo)入
到目前為止介紹的都是靜態(tài)導(dǎo)入模塊,靜態(tài)導(dǎo)入必須等模塊代碼全部下載之后才會執(zhí)行程序,這可能會使網(wǎng)站的首屏渲染性能下降。通過動態(tài)導(dǎo)入模塊可以根據(jù)用戶在界面上的操作按需下載資源,節(jié)省流量,動態(tài)導(dǎo)入在 ECMAScript 2020 正式發(fā)布,它需要用到import(),用法如下所示:
// 通過相對路徑導(dǎo)入
import('./exportDefault.js').then((module) => {
console.log(module) // line A
})
// 通過絕對路徑導(dǎo)入
import('https://cdn.skypack.dev/react').then((react) => {
console.log(react) // line B
})
從上述代碼可以看出 import() 的返回值是一個 promise 對象,當模塊加載成功之后 promise 對象的狀態(tài)會變成 fulfilled,import() 可以與 async/await 配合使用
上述代碼中的 line A 和 line B 標識的變量 module 和 react 都是 JavaScript 對象,我們可以用對象的點語法和中括號語法訪問模塊導(dǎo)出的任何方法和屬性,模塊的默認導(dǎo)出通過 default 屬性名訪問。
動態(tài)導(dǎo)入與靜態(tài)導(dǎo)入存在如下 3 個差異:
- 動態(tài)導(dǎo)入的模塊說明符可以是變量,但靜態(tài)導(dǎo)入的模塊說明符只能是字符串字面量
- 動態(tài)導(dǎo)入能在模塊和常規(guī)腳本中使用,但是靜態(tài)導(dǎo)入只能在模塊中使用
- 動態(tài)導(dǎo)入不必位于文件的頂層,但靜態(tài)導(dǎo)入只能位于模塊的頂層
雖然動態(tài)導(dǎo)入模塊和靜態(tài)導(dǎo)入模塊存在差異,但它們都通過 CORS 獲取模塊腳本,所以在獲取跨域模塊腳本時,腳本的 Access-Control-Allow-Origin 響應(yīng)頭一定要配置正確。
動態(tài)導(dǎo)入和靜態(tài)導(dǎo)入有它們各自的使用場景。在初始渲染時要用到的模塊使用靜態(tài)導(dǎo)入,其他情況,特別是那些與用戶操作相關(guān)的功能,可以使用動態(tài)導(dǎo)入按需加載依賴的模塊,這種做法能提高首屏渲染性能,但是會降低用戶操作過程中的性能。所以,哪些模塊使用靜態(tài)導(dǎo)入,哪些模塊使用動態(tài)導(dǎo)入需要你根據(jù)實際情況考慮。
提示:在動態(tài)導(dǎo)入模塊時要用到 import(),看上去這像是函數(shù)調(diào)用,實際上它并不是函數(shù)調(diào)用,而是一種特殊的語法,你不能使用 import.call()、import.apply()、const myImport = import; myImport()。
以上就是ECMAScript modules規(guī)范示例詳解的詳細內(nèi)容,更多關(guān)于ECMAScript modules規(guī)范的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript?CSS優(yōu)雅實現(xiàn)網(wǎng)頁多主題風格換膚功能詳解
這篇文章主要為大家介紹了JavaScript?CSS優(yōu)雅的實現(xiàn)網(wǎng)頁多主題風格換膚功能詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
JS前端架構(gòu)pnpm構(gòu)建Monorepo方式管理demo
這篇文章主要為大家介紹了JS前端架構(gòu)pnpm構(gòu)建Monorepo方式的管理demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07
微信小程序 頁面跳轉(zhuǎn)和數(shù)據(jù)傳遞實例詳解
這篇文章主要介紹了微信小程序 頁面跳轉(zhuǎn)和數(shù)據(jù)傳遞實例詳解的相關(guān)資料,這里附有實例代碼幫助到家學習理解,需要的朋友可以參考下2017-01-01
微信小程序報錯:this.setData is not a function的解決辦法
這篇文章主要介紹了微信小程序報錯:this.setData is not a function的解決辦法的相關(guān)資料,希望通過本文能幫助到大家解決這樣類似的問題,需要的朋友可以參考下2017-09-09

