用模組封裝程式碼
JavaScript 用「共享一切」的方法載入程式碼,這是該語言中最容易出錯且令人感到困惑的地方。其他語言使用諸如套件 (package) 這樣的概念來定義程式碼作用域 (scope)。在 ES6 以前,在應用程式的每一個 JavaScript 檔案中定義的一切都共享一個全域作用域 (global scope)。隨著 Web 應用程式變得更加複雜,JavaScript 程式碼的使用量也開始增長,這一做法會引起問題,如命名衝突和安全性問題。ES6 的一個目標是解決作用域問題,也為了使用 JavaScript 應用程式顯得有序,於是引進了模組 (module)。
什麼是模組
模組 (module) 是自動運行在嚴格模式 (strict mode) 下並且沒有辦法退出運行的 JavaScript 程式碼。與共享一切架構相反的是,在模組頂部建立的變數不會自動被添加到全域共享作用域 (global shared scope),這個變數僅在模組的頂層作用域 (top-level scope) 存在,而且模組必須匯出 (export) 一些外部程式碼可以存取的元素,如變數或函式。模組也可以從其他模組匯入 (import) 繫結 (binding)。
另外兩個模組的特性與作用域 (scope) 關係不大,但也很重要。首先,在模組的頂部,this 的值是 undefined;其次,模組不支援 HTML 風格的程式碼註解,這是從早期瀏覽器殘餘下來的 JavaScript 的特性。
腳本 (script),也就是任何不是模組的 JavaScript 程式碼,則缺少這些特性。模組和其他 JavaScript 程式碼之間的差異可能乍看不起眼 (好像一樣)。但是它們代表了 JavaScript 程式碼載入與求值的一個重要變化。模組真正魔力所在是僅匯出與匯入你需要的繫結 (binding),而不是將所有的東西都放到一個檔案。只有很好的理解了匯出與匯入才能理解模組與腳本的區別。
匯出的基本語法
可以使用export關鍵字 (keyword) 將一部分已發布的程式碼暴露給其他模組 (module),在最簡單的用例中,可以將 export 放在任何變數、函式或類別宣告的前面,以將它們從模組中匯出 (export)。如:
// 匯出資料
export var color = "red"
export let name = "Nicholas";
export const magicNumber = 7;
// 匯出函式
export funciton sum(num1,num2){
return num1+num2
}
// 匯出類別
export class Rectangle {
constructor(length,width) {
this.length = length;
this.width = width
}
}
// 定義一個函式
function multiply(num1,num2){
return num1 * num2;
}
// ...之後將它匯出
export multiply
注意到,除了
export關鍵字 (keyword) 外,每一個宣告與腳本 (script) 中的一模一樣,因為匯出的函式與類別宣告需要一個名稱,所以程式碼中的每一個函式或類別也確實有這個名稱。除非用default關鍵字,所以不能用export直接匯出一個匿名函式或類別,multiply在定義它時沒有馬上匯出它,由於不必總是匯出宣告,可以匯出參考。
任何未顯示匯出 (export) 的變數、函式或類別都是模組私有的,無法從模組外部存取的
匯入語法
import { identifier1, identifier2 } from './example.js';
匯入繫結 (import binding) 的列表看起來與解構物件 (destructuring object) 很相似,但它不是,從模組 (module) 中匯入一個繫結時,它就好像使用了 const 定義的一樣。結果是你無法定義另一個同名變數(包括匯入另一個同名繫結)也無法在import陳述式前使用識別符 (identifier) 或改變繫結的值。
// 匯入一個
import { sum } from './example.js'
console.log(sum(1,2)) //3
sum = 1 //拋出一個錯誤
// 不能給匯入的繫結重新賦值
// 匯入多個繫結
import { sum, multiply, magicNumber } from './example.js'
console.log(sum(1, magicNumber)); //8
console.log(multiply(1,2)); // 2
// 匯入整個模組作為一個單一的物件。然後所有匯出都可以作為物件的屬性使用
import * as exapmle from './example.js'
console.log(exapmle.sum(1, magicNumber)); //8
console.log(exapmle.multiply(1,2)); // 2
import * as exapmle from './example.js' 這種引入格式被稱作命名空間匯入 (namespace import)。因為 example.js 檔案中不存在example物件,故而它作為 example.js 中所有匯出成員的命名空間物件 (namespace object) 而被建立。但是,請注意,不管在import陳述式中把一個模組寫了多少次,該模組將只執行一次。,匯入模組的程式碼執行後,已實例化的模組被保存在記憶體中,只要另一個import陳述式引用它就可以重複使用它。
import { smu } from './example.js';
import { multiply } from './example.js';
import { magicNumber } from './example.js';
儘管模組 (module) 中有 3 個 import 陳述式,但 example.js 將只執行一次,如果同一個應用程式中的其它模組也從 example.js 中匯入繫結,那麼那些模組與此程式碼使用的是相同的模組。
import和export的一個重要限制是,它們必須在其他陳述式與函式之外使用(if else funciton中)。匯入繫結的一個微妙怪異之處
ES6 的 import 陳述式為變數、函式與類別建立的是唯讀繫結 (read-only binding),而不是像正常變數一樣簡單地引用原始繫結。識別符 (identifier) 只有在被匯出模組中可以修改,即便是匯入繫結的模組也無法更改繫結的值,如:
// example.js
export var name = "Nicholas";
export funtion setName(newName){
name = newName
}
// 使用模組
import { name, setName } from './example.js'
console.log(name); // Nicholas
setName("Greg");
// 會回到匯出 setName() 的模組中去執行,並將 name 設定為 Greg
// 然後 name 的更改會體現在(自動)匯入 name 模組中,
// 匯入的 name 是匯出的 name 識別的本地名稱。
// 兩個 name 不是同一回事兒
console.log(name); // Greg
name = "hehe" // 拋出錯誤
匯入與匯出的
as使用方式
function sum(num1, num2) {
return num1 + num2;
}
export { sum as add}
// sum 是以 add 名匯出的,引入時只能引用 add
import { add } from './sample.js';
// 匯入也可以使用 as 給它重命名
import { add as sum } from './sample.js'
// 目前上下文中只有 sum 沒有 add
預設值 (default value),由於諸如 CommonJS(瀏覽器外的另一個 JavaScript 使用規範)的其它模組系統 (module system) 中,從模組中匯出與匯入預設值是一個常見的做法,該語法 (syntax) 被進行最佳化。模組的預設值指的是透過
default關鍵字 (keyword) 指定的單一變數、函式或類別,只能為每個模組設定一個預設匯出值,匯出時多次使用default關鍵字是一個語法錯誤 (syntax error)。default是一個關鍵字,不能出現在變數中
以下是匯出預設值的 3 種方式
// 1
export default funciton(num1, num2) {
return num1 + num2
}
// 2
function sum(num1, num2) {
return num1 + num2
}
export default sum
// 3
function sum(num1, num2) {
return num1 + num2
}
export {sum as default}
匯入預設值的使用方式
import sum from './sample.js'
console.log(sum(1,2)) //3
// 匯入預設值及非預設值的混合
// example.js
export let color = 'red';
export default function(num1, num2) {
return num1 + num2
}
// 使用時
import sum,{ color } from './sample.js'
// 預設匯出的被重命名為 sum, 並且匯入了 color
重新匯出已匯入的值
// 1
import { sum } from './example.js'
export { sum }
// 也可以透過一條陳述式來完成這個功能
export { sum } from './example.js'
// 或者給它起個別的名字匯出
export { sum as add } from './example.js'
無繫結匯入 (bare import)
某些模組 (module) 可能不匯出 (export) 任何東西,相反它們可能只修改全域作用域 (global scope) 中的物件。儘管模組中的頂層變數、函式與類別不自動地出現在全域作用域中,但這並不意味著模組無法存取全域作用域。內建物件 (built-in object) (如 Array 和 Object) 的共享定義可以在模組中存取,對這些物件所做的更改將反映在其它模組中。
如我們給 Array 添加一個 pushAll 方法,無繫結匯入 (bare import) 最有可能被應用於建立 Polyfill 和 Shim
// sample.js
// 沒有匯出也沒有匯入
Array.prototype.pushAll = function(items){
if(!Array.isArray(items)){
throw new TypeError('參數必須是一個陣列')
}
return this.push(...items)
}
// 使用
import './example.js' //沒匯入任何,僅執行了一下
let colors = ["red", "green", "blue"];
let items = []
items.pushAll(colors)
在 Web 應用程式中使用模組
ES6 以前,網頁瀏覽器 (web browser) 也有多種方式可以將 JavaScript 包含在 Web 應用程式 (web application) 中,這些腳本 (script) 載入的方式:
- 在
<script>元素中透過src屬性指定載入程式碼的位址來載入 JavaScript 程式碼檔案 - 將 JavaScript 程式碼內嵌到沒有
src屬性的<script>元素中 - 透過 Web Worker 或 Service Worker 的方式載入並執行 JavaScript 程式碼檔案
為了完全支援模組功能,網頁瀏覽器 (web browser) 必須更新這些機制,具體說明總結如下:
在
<script>中使用模組
非模組載入時,當 type 屬性缺失或包含一個 JavaScript 內容類型"text/javascript"時是作為腳本載入,<script>元素可以執行內嵌程式碼或載入 src 指定的檔案,當 type 屬性值為type="module"時支援載入模組,這種模式下可以讓瀏覽器將所有內嵌程式碼或包含在 src 指定的檔案中的程式碼按照模組 (module) 而非腳本 (script) 的方式載入。區分是模組還是腳本就是看這個type="module"與否
<!-- 載入一個 JavaScript 模組檔案 -->
<script type="module" src="module.js"></script>
<!-- 內嵌引入一個模組 -->
<script type="module">
import { sum } from './example.js'
let result = sum(1,2);
</script>
網頁瀏覽器中模組的載入順序
模組載入時,defer這個可選屬性是必須的(預設就是這種,不一定要寫在那),因為模組與腳本不同它是獨一無二的,可以透過 import 關鍵字 (keyword) 來指明其所依賴的其他檔案,並且這些檔案被載入該模組才能正確執行。
模組是按照它們出現在 HTML 檔案中的順序執行,也就是說,無論模組中包含的是內嵌程式碼還是指定的 src 屬性,第一個<script type="module">總是在第二個之前執行,如下
<!-- 先執行這個 -->
<script type="module" src="module1.js"></script>
<!--第二執行這個 -->
<script type="module">
import { sum } from './example.js'
let result = sum(1,2);
</script>
<!-- 最後執行這個 -->
<script type="module" src="module2.js"></script>
因為每個模組都可以從一個或多個其它的模組匯入 (import),這會使問題複雜化。因此,首先解析模組以識別所有匯入陳述式 (import statement),然後,每個匯入陳述式都觸發一次獲取過程 (fetching process)(從網路或快取 (cache)),並且在所有匯入資源被載入與執行後才會執行目前模組。
用<script type="module">顯示引入和用 import 隱式匯入的所有模組都是按需載入並執行的。 執行過程描述如下
- 下載並解析
module1.js - 遞迴下載並解析
module1.js中匯入的資源 - 解析內嵌模組
- 遞迴下載並解析內嵌模組中匯入的資源
- 下載並解析
module2.js - 遞迴下載並解析
module2.js中匯入的資源
載入完成後,只有當文件完全被解析之後才會執行其它操作,文件解析後,會發生以下操作
- 遞迴執行
module1.js中匯入的資源 - 執行
module1.js - 遞迴執行內嵌模組中匯入的資源
- 執行內嵌模組
- 遞迴執行
module2.js中匯入的資源 - 執行
module2.js
網頁瀏覽器中的非同步模組載入
script中的asnyc屬性,當其應用於腳本 (script) 時,腳本檔案將在檔案完全下載並解析後執行。但是,文件中的 asnyc 腳本的順序不會影響腳本的執行順序,腳本在下載完成後立即執行,而不必等待包含的文件完成解析。這個屬性應用於模組 (module) 時,情況類似,唯一區別是,在模組執行前,模組中所有的匯入資源都必須下載下來。這可以大確保只有當模組執行所需的所有資源都下載完成後才執行模組,但不能保證模組的執行時機。
<!-- 不能保證哪個先執行,哪個先把相關資源下載完,哪個先執行 -->
<script type="module" src="module1.js" asnyc></script>
<script type="module" src="module2
將模組作為 Worker 載入
// 第二個參數用來指定類型
let worker = new Worker('module.js',{type:'module'})
瀏覽器模組說明符解析
瀏覽器要求模組說明符 (module specifier) 具有以下幾個格式之一:
- 以
/開頭的解析為從根目錄開始 - 以
./開頭的解析為從目前目錄開始 - 以
../開頭的解析為從父目錄開始 - URL 格式
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/4339