ECMAScript?modules規(guī)范示例詳解
引言
很多編程語言都有模塊這一概念,JavaScript 也不例外,但在 ECMAScript 2015 規(guī)范發(fā)布之前,JavaScript 沒有語言層面的模塊語法。模塊實際上是一種代碼重用機(jī)制,要實現(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 的標(biāo)準(zhǔn)模塊系統(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種寫法
- 默認(rèn)導(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 中除默認(rèn)導(dǎo)出之外的內(nèi)容 export * from './other_module.js' // 重新導(dǎo)出 other_module 中的默認(rèn)導(dǎo)出 export { default } from './other_module.js' // 重新導(dǎo)出 other_module 中的默認(rèn)導(dǎo)出,并且將 other_module 中的 sayName 命名為 getName 之后再導(dǎo)出 export { default, sayName as getName } from './other_module.js'
雖然模塊導(dǎo)出有4種寫法,但是只有兩種方式,一種默認(rèn)導(dǎo)出,另一種是命名導(dǎo)出,在同一個模塊中命名導(dǎo)出可以有多個,默認(rèn)導(dǎo)出只能有一個,這兩種方式可以混合使用。
在軟件開發(fā)的過程中,通常有多種寫法能到達(dá)同一目的,但并不是每一種寫法都值得推薦,模塊導(dǎo)出也是類似的。如果在同一個模塊中,即有默認(rèn)導(dǎo)出,又有行內(nèi)命名導(dǎo)出,還有 export 子句批量命名導(dǎo)出,那么你的模塊很可能會變得混亂。在這里我推薦使用默認(rèn)導(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'
相對路徑說明符以 / 、./ 、../ 開頭,當(dāng)使用相對路徑說明符時不能省略文件的擴(kuò)展名。在 web 項目開發(fā)中使用相對路徑導(dǎo)入模塊的時候,你可能省略了文件擴(kuò)展名,它還是能夠工作,那是因為你的項目使用了如 webpack 這樣的模塊打包工具。
- 絕對路徑
import React from 'https://cdn.skypack.dev/react'
上述代碼表示從 cdn 導(dǎo)入模塊,當(dāng)使用絕對路徑導(dǎo)入模塊時,是否能省略文件擴(kuò)展名,這與服務(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種不同的寫法,如下:
- 默認(rèn)導(dǎo)入
// 你可以將 myFunc 改成任何你喜歡的變量名 import myFunc from './myModule.js'
- 將模塊作為一個對象導(dǎo)入(即命名空間導(dǎo)入)
import * as api from './myModule.js' // 通過對象的 default 屬性訪問 myModule.js 中的默認(rèn)導(dǎo)出 console.log(api.default)
- 命名導(dǎo)入
// 導(dǎo)入 myModule.js 中的fooStr import { fooStr } from './myModule.js' // 將myModule.js中默認(rèn)導(dǎo)出命名為myFunc import { default as myFunc } './myModule.js' // 將 myModule.js中的 fooStr 命名為 myStr import { fooStr as myStr } from './myModule.js'
當(dāng)某個模塊中導(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)入到當(dāng)前模塊,但是會執(zhí)行 myModule.js 模塊體,這通常用于執(zhí)行一些初始化操作。
- 將默認(rèn)導(dǎo)入與命名導(dǎo)入混合使用
import myFunc, { fooStr } from './myModule.js'
- 將默認(rèn)導(dǎo)入與命名空間導(dǎo)入混合使用
import myFunc, * as api from './myModule.js'
補(bǔ)充:同一個模塊可以被多次導(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)該避免,因為這會使兩個模塊強(qiá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腳本的差異
- 模塊運行在嚴(yán)格模式下
- 模塊具備詞法頂部作用域
這句話的意思是,在模塊中創(chuàng)建的變量,如:foo,不能通過 window.foo 訪問。代碼如下:
var foo = 'hi' console.log(window.foo) // undefined console.log(foo) // hi export {} // 將這個文件標(biāo)記成模塊
在模塊中的聲明的變量是針對該模塊的,這意味著在模塊中聲明的任何變量對其他模塊都不可用,除非它們被顯式地導(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 腳本,所以在寫代碼的時候做好顯示地標(biāo)記 JavaScript 文件是模塊,只要 JavaScript 文件中包含 export 或者 import 關(guān)鍵字,JavaScript 運行時就會認(rèn)為這個文件是模塊
在這部分介紹的這 5 個差異是與 JavaScript 運行環(huán)境無關(guān)的差異,在之后的部分會介紹在瀏覽器中使用 ES modules,這那里會補(bǔ)充一些新的差異。
在瀏覽器中使用 ES modules
現(xiàn)代瀏覽器支持 ES modules,你可以將 script 標(biāo)簽的 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 解析
默認(rèn)情況,當(dāng)瀏覽器下載常規(guī)外部腳本時,它會暫停解析 HTML,我們可以在 script 標(biāo)簽上添加 defer 屬性,使瀏覽器在下載腳本期間不暫停解析 HTML。當(dāng)下載模塊腳本時,瀏覽器默認(rèn)模塊腳本是 defer 的。
下圖展示了瀏覽器獲取外部模塊腳本和常規(guī)腳本的流程
上圖源于 (html.spec.whatwg.org/multipage/s…)
- 模塊腳本通過 CORS 獲取
模塊腳本以及它的依賴項是通過 CORS 獲取的,當(dāng)獲取一個跨域的模塊腳本時需要特別注意這個問題,跨域腳本的響應(yīng)頭 Access-Control-Allow-Origin 必須包含當(dāng)前域,否則模塊會獲取失敗,而獲取常規(guī)腳本則沒有這個限制。
為了保證獲取同源模塊腳本時,瀏覽器始終帶上 credentials(cookie 等),推薦給 script 標(biāo)簽加上 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 對象,當(dāng)模塊加載成功之后 promise 對象的狀態(tài)會變成 fulfilled,import() 可以與 async/await 配合使用
上述代碼中的 line A 和 line B 標(biāo)識的變量 module 和 react 都是 JavaScript 對象,我們可以用對象的點語法和中括號語法訪問模塊導(dǎo)出的任何方法和屬性,模塊的默認(rèn)導(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ī)范示例詳解的詳細(xì)內(nèi)容,更多關(guān)于ECMAScript modules規(guī)范的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序 es6-promise.js封裝請求與處理異步進(jìn)程
這篇文章主要介紹了微信小程序 es6-promise.js封裝請求與處理異步進(jìn)程的相關(guān)資料,需要的朋友可以參考下2017-06-06微信小程序?qū)崿F(xiàn)緩存根據(jù)不同的id來進(jìn)行設(shè)置和讀取緩存
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)緩存根據(jù)不同的id來進(jìn)行設(shè)置和讀取緩存的相關(guān)資料,需要的朋友可以參考下2017-06-06arrify 轉(zhuǎn)數(shù)組實現(xiàn)示例源碼解析
這篇文章主要為大家介紹了arrify 轉(zhuǎn)數(shù)組實現(xiàn)示例源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12