你不容錯過的JavaScript高階語法(模組化)

語言: CN / TW / HK

theme: qklhk-chocolate highlight: a11y-dark


眾所周知,js在前端開發中的地位。學好它,真的很重要。

下面這篇文章,介紹一下模組化。

什麼是模組化?

到底什麼是模組化、模組化開發呢? - 事實上模組化開發最終的目的是將程式劃分成一個個小的結構。 - 這個結構中編寫屬於自己的邏輯程式碼,有自己的作用域,不會影響到其他的結構。 - 這個結構可以將自己希望暴露的變數、函式、物件等匯出給其結構使用。 - 也可以通過某種方式,匯入另外結構中的變數、函式、物件等。 上面說提到的結構,就是模組;按照這種結構劃分開發程式的過程,就是模組化開發的過程。

模組化的歷史

在網頁開發的早期,Brendan Eich開發JavaScript僅僅作為一種指令碼語言,做一些簡單的表單驗證或動畫實現等,那個時候程式碼還是很少的: - 這個時候我們只需要講JavaScript程式碼寫到

js //main.js require.config({ baseUrl: '', // 預設是main.js的資料夾路徑 paths: { foo: "./foo" } })

require(["foo"], function(foo) {
  console.log("main:", foo)
})

js // foo.js define(function() { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 }

  return {
    name,
    age,
    sum
  }
})

```

CMD規範

CMD規範也是應用於瀏覽器的一種模組化規範: - CMD 是Common Module Definition(通用模組定義)的縮寫。它也採用了非同步載入模組,但是它將CommonJS的優點吸收了過來。 - AMD實現的比較常用的庫是SeaJS。 SeaJS的使用 - 下載原始碼:https://github.com/seajs/seajs。 找到dist資料夾下的sea.js。 - 引入sea.js和使用主入口檔案。 html // index.html <script src="./sea.js"></script> <script> seajs.use("./main.js") </script> js //main.js define(function(require, exports, module) { const foo = require("./foo") console.log("main:", foo) }) ```js // foo.js define(function(require, exports, module) { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 }

  // exports.name = name
  // exports.age = age

  module.exports = {
    name,
    age,
    sum
  }
});

```

ES Module

ES Module和CommonJS的模組化有一些不同之處: - 一方面它使用了import和export關鍵字來實現模組化。 - 另一方面它採用編譯期的靜態分析,並且也加入了動態引用的方式。 - export負責將模組內的內容匯出。 - import負責從其他模組匯入內容。 - 採用ES Module將自動採用嚴格模式:use strict。

基本使用

html // index.html <script src="./main.js" type="module"></script> ```js // foo.js let obj = { name: "zh", age: 22 }

export default sum

js // main.js import foo from './foo.js' console.log(foo) ``` - 在html檔案載入入口檔案的時候,需要指定type為module。 - 在開啟html檔案時,需要開啟本地服務,而不能直接開啟執行在瀏覽器上。 image.png 這個在MDN上面有給出解釋: - 你需要注意本地測試 — 如果你通過本地載入Html 檔案 (比如一個 file:// 路徑的檔案), 你將會遇到 CORS 錯誤,因為Javascript 模組安全性需要。 - 你需要通過一個伺服器來測試。

exports關鍵字

export關鍵字將一個模組中的變數、函式、類等匯出。

我們希望將其他中內容全部匯出,它可以有如下的方式: - 方式一:在語句宣告的前面直接加上export關鍵字。 js export const name = "zh" export const age = 22 - 方式二:將所有需要匯出的識別符號,放到export後面的 {} 中。注意:這裡的 {}裡面不是ES6的物件字面量的增強寫法,{}也不是表示一個物件的。所以: export {name: name},是錯誤的寫法。 ```js const name = "zh" const age = 22 function foo() { console.log("foo function") }

export {
  name,
  age,
  foo
}

- 方式三:匯出時給識別符號起一個別名。(基本沒用,一般在匯入檔案中起別名)。然後在匯入檔案中就只能使用別名來獲取。js export { name as fName, age as fAge, foo as fFoo } ```

import關鍵字

import關鍵字負責從另外一個模組中匯入內容。

匯入內容的方式也有多種: - 方式一:import {識別符號列表} from '模組'。注意:這裡的{}也不是一個物件,裡面只是存放匯入的識別符號列表內容。 js import { name, age } from "./foo.js" - 方式二:匯入時給識別符號起別名。 js import { name as fName, age as fAge } from './foo.js' - 方式三:通過 * 將模組功能放到一個模組功能物件(a module object)上。然後通過起別名來使用。 js import * as foo from './foo.js'

export和import結合使用

表示匯入匯出。 ```js import { add, sub } from './math.js' import {otherProperty} from './other.js'

export {
  add,
  sub,
  otherProperty
}

等價於js // 匯入的所有檔案會統一被匯出 export { add, sub } from './math.js' export {otherProperty} from './other.js' ``` 等價於

js export * from './math.js' export * from './other.js' 為什麼要這樣做呢?

在開發和封裝一個功能庫時,通常我們希望將暴露的所有介面放到一個檔案中。 這樣方便指定統一的介面規範,也方便閱讀。這個時候,我們就可以使用export和import結合使用。

default用法

前面我們學習的匯出功能都是有名字的匯出(named exports): - 在匯出export時指定了名字。 - 在匯入import時需要知道具體的名字。

還有一種匯出叫做預設匯出(default export) ```js // foo.js const name = "zh" cconst age = 22 export { name, // 或者這樣的預設匯出 // age as default }

export default age

js // 匯入語句: 匯入的預設的匯出 import foo, {name} from './foo.js'

console.log(foo, name) // 22 zh

``` - 預設匯出export時可以不需要指定名字。 - 在匯入時不需要使用 {},並且可以自己來指定名字。 - 它也方便我們和現有的CommonJS等規範相互操作。 注意:在一個模組中,只能有一個預設匯出(default export)。

import函式

通過import載入一個模組,是不可以在其放到邏輯程式碼中的,比如: js if(true) { import foo from './foo.js' } 為什麼會出現這個情況呢? - 這是因為ES Module在被JS引擎解析時,就必須知道它的依賴關係。 - 由於這個時候js程式碼沒有任何的執行,所以無法在進行類似於if判斷中根據程式碼的執行情況。

但是某些情況下,我們確確實實希望動態的來載入某一個模組: - 如果根據不同的條件,動態來選擇載入模組的路徑。 這個時候我們需要使用 import() 函式來動態載入。import函式返回的結果是一個Promise。 js import("./foo.js").then(res => { console.log("res:", res.name) }) es11新增了一個屬性。meta屬性本身也是一個物件: { url: "當前模組所在的路徑" } js console.log(import.meta)

ES Module的解析流程

ES Module是如何被瀏覽器解析並且讓模組之間可以相互引用的呢?

詳細請參考這篇文章

ES Module的解析過程可以劃分為三個階段: - 階段一:構建(Construction),根據地址查詢js檔案,並且下載,將其解析成模組記錄(Module Record)。 - 階段二:例項化(Instantiation),對模組記錄進行例項化,並且分配記憶體空間,解析模組的匯入和匯出語句,把模組指向對應的記憶體地址。 - 階段三:執行(Evaluation),執行程式碼,計算值,並且將值填充到記憶體地址中。 image.png 階段一: image.png 階段二和三: image.png 所以,從上面可以看出在匯出檔案中,修改變數的值會影響到匯入檔案中的值。而且匯入檔案被限制修改匯出檔案的值。

es6 module 和common.js的區別