一文徹底搞定es6模組化
前情回顧
- 在上篇文章中我們講到了
CommonJs
,如果還沒看,可以查找本文章所在的專欄進行學習。 -
CommonJs
有很多優秀的功能,下面我們再簡單的回顧一下:
模組程式碼只在載入後運行;
模組只能載入一次;
模組可以要求載入其他模組;
支持循環依賴;
模組可以定義公共介面,其他模組可以基於這個公共介面觀察與互動;
天下苦CommonJs 久矣
-
Es Module
的獨特之處在於,既可以透過瀏覽器原生載入,也可以與第三方載入器和建置工具一起載入。 - 支援
Es module
模組的瀏覽器可以從頂級模組載入整個依賴圖,且是非同步完成。瀏覽器會解析入口模組,確定依賴,並傳送對依賴模組的請求。這些檔案透過網路返回後,瀏覽器就會解析它們的依賴,,如果這些二級依賴還沒有載入,則會發送更多請求。 - 這個非同步遞歸載入過程會持續到整個應用程式的依賴圖都解析完成。解析完成依賴圖,引用程式就可以正式載入模組了。
-
Es Module
不只借用了CommonJs
和AMD
的許多優秀特性,還增加了一些新行為:
#Es Module
預設在嚴格模式下執行;#Es Module
不共享全域命名空;Es Module
頂層的this
# 的值是undefined
(常規腳本是window
# );模組中的
var
宣告不會加入到window
物件;##Es Module
是非同步載入和執行的;
- 模組功能主要由兩個指令構成:
- exports
和
import。
- export
指令用來規定模組的對外接口,
import指令用來輸入其他模組提供的功能。
- 導出的基本形式:
export const nickname = "moment"; export const address = "广州"; export const age = 18;
- 當然了,你也可以寫成以下的形式:
const nickname = "moment"; const address = "广州"; const age = 18; export { nickname, address, age };
- 對外導出一個物件和函數
export function foo(x, y) { return x + y; } export const obj = { nickname: "moment", address: "广州", age: 18, }; // 也可以写成这样的方式 function foo(x, y) { return x + y; } const obj = { nickname: "moment", address: "广州", age: 18, }; export { foo, obj };
- 通常情況下,
- export
輸出的變數就是本來的名字,但是可以使用
as關鍵字重命名。
const address = "广州"; const age = 18; export { nickname as name, address as where, age as old };
- 預設導出,值得注意的是,一個模組只能有一個預設導出:
export default "foo"; export default { name: 'moment' } export default function foo(x,y) { return x+y } export { bar, foo as default };
- 導出語句必須在模組頂層,不能嵌套在某個區塊中:
if(true){ export {...}; }
- #export
必須提供對外的介面:
// 1只是一个值,不是一个接口export 1// moment只是一个值为1的变量const moment = 1export moment// function和class的输出,也必须遵守这样的写法function foo(x, y) { return x+y }export foo复制代码
- 使用
- export
指令定義了模組的對外介面以後,其他js檔案就可以透過
import指令加載整個模組
import {foo,age,nickname} from '模块标识符'
- 模組識別碼可以是目前模組的相對路徑,也可以是絕對路徑,也可以是純字串,但不能是動態計算的結果,例如憑藉的字串。
- import
指令後面接受一個花括弧,裡面指定要從其他模組匯入的變數名稱,而且變數名稱必須與被匯入模組的對外介面的名稱相同。
對於導入的變數不能對其重新賦值,因為它是一個只讀介面,如果是一個物件,可以對這個物件的屬性重新賦值。導出的模組可以修改值,導入的變數也會跟著改變。
#
- 从上图可以看得出来,对象的属性被重新赋值了,而变量的则报了
Assignment to constant variable
的类型错误。 - 如果模块同时导出了命名导出和默认导出,则可以在
import
语句中同时取得它们。可以依次列出特定的标识符来取得,也可以使用*
来取得:
// foo.js export default function foo(x, y) { return x + y; } export const bar = 777; export const baz = "moment"; // main.js import { default as foo, bar, baz } from "./foo.js"; import foo, { bar, baz } from "./foo.js"; import foo, * as FOO from "./foo.js";
动态 import
- 标准用法的
import
导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。 - 关键字
import
可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个promise
。
import("./foo.js").then((module) => { const { default: foo, bar, baz } = module; console.log(foo); // [Function: foo] console.log(bar); // 777 console.log(baz); // moment});复制代码
使用顶层 await
- 在经典脚本中使用
await
必须在带有async
的异步函数中使用,否则会报错:
import("./foo.js").then((module) => { const { default: foo, bar, baz } = module; console.log(foo); // [Function: foo] console.log(bar); // 777 console.log(baz); // moment });
- 而在模块中,你可以直接使用
Top-level await
:
const p = new Promise((resolve, reject) => { resolve(777); });const result = await p;console.log(result); // 777正常输出
import 的错误使用
- 由于
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 错误 import { 'b' + 'ar' } from './foo.js'; // 错误 let module = './foo.js'; import { bar } from module; // 错误 if (x === 1) { import { bar } from './foo.js'; } else { import { foo } from './foo.js'; }
在浏览器中使用 Es Module
- 在浏览器上,你可以通过将
type
属性设置为module
用来告知浏览器将script
标签视为模块。
<script></script><script></script>
- 模块默认情况下是延迟的,因此你还可以使用
defer
的方式延迟你的nomodule
脚本:
<script> console.log("模块情况下的"); </script> <script></script> <script> console.log("正常 script标签"); </script>
- 在浏览器中,引入相同的
nomodule
脚本会被执行多次,而模块只会被执行一次:
<script></script> <script></script> <script></script> <script></script> <script></script>
模块的默认延迟
- 默认情况下,
nomodule
脚本会阻塞HTML
解析。你可以通过添加defer
属性来解决此问题,该属性是等到HTML
解析完成之后才执行。
-
defer
和async
是一個可選屬性,他們只可以選擇其中一個,在nomodule
腳本下,defer
等到HTML
解析完才會解析目前腳本,而async
會和HTML
並行解析,不會阻塞HTML
的解析,模組腳本可以指定async
屬性,但對於defer
無效,因為模組預設就是延遲的。 - 對於模組腳本,如果存在
async
屬性,模組腳本及其所有依賴項將於解析並行獲取,並且模組腳本將在它可用時進行立即執行。
Es Module 和Commonjs 的差異
- 討論
Es Module
模組之前,必須先了解Es Module
與Commonjs
完全不同,它們有三個完全不同:
-
CommonJS
模組輸出的是一個值的拷貝,Es Module
輸出的是值的參考; -
CommonJS
模組是運行時加載,Es Module
是編譯時輸出介面。 -
CommonJS
模組的require()
是同步加載模組,ES6 模組的import
命令是異步加載,有一個獨立的模組依賴的解析階段。
- 第二個差異是因為
CommonJS
載入的是物件(即module.exports
屬性),該物件只有在腳本執行完才會生成。而Es Module
不是對象,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會產生。 -
Commonjs
輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就會影響不到這個值。具體可以看上一篇寫的文章。 -
Es Module
的運作機制與CommonJS
不一樣。JS引擎
對腳本靜態分析的時候,遇到模組載入指令import
,就會產生一個唯讀參考。等到腳本真正執行時,再根據這個唯讀引用,到被載入的那個模組裡面去取值。換句話說,import
就是一個連接管道,原始值改變了,import
載入的值也會跟著變。因此,Es Module
是動態引用,並且不會快取值,模組裡面的變數綁定其所在的模組。
Es Module 工作原理的相關概念
- 在學習工作原理之前,我們不妨來認識一下相關的概念。
Module Record
- 模組記錄(
Module Record
) 封裝了關於單一模組(目前模組)的匯入和匯出的結構資訊。此資訊用於連結連接模組集的匯入和匯出。一個模組記錄包括四個字段,它們只在執行模組時使用。其中這四個欄位分別是:
-
Realm
: 建立目前模組的作用域; -
Environment
:模組的頂層綁定的環境記錄,該字段在模組被鏈接時設置; -
Namespace
:模組命名空間對像是模組命名空間外來對象,它提供對模組導出綁定的基於運行時屬性的存取。模組命名空間物件沒有建構子; -
HostDefined
:欄位保留,以按host environments
使用,需要將附加資訊與模組關聯。
Module Environment Record
- 模組環境記錄是一種宣告式環境記錄,用來表示ECMAScript模組的外部作用域。除了普通的可變和不可變綁定之外,模組環境記錄還提供了不可變的
import
綁定,這些綁定提供了對存在於另一個環境記錄中的目標綁定的間接存取。
不可變綁定就是當前的模組引入其他的模組,引入的變數不能修改,這就是模組獨特的不可變綁定。
Es Module 的解析流程
- 在開始之前,我們先大概了解一下整個流程大概是怎麼樣的,先有一個大概的了解:
- 階段一:建構(
Construction
),根據位址尋找js
檔案,透過網路下載,並且解析模組檔案為Module Record
; - 階段二:實例化(
Instantiation
),對模組進行實例化,並且分配記憶體空間,解析模組的導入與導出語句,把模組指向對應的記憶體位址; - 階段三:運行(
Evaluation
),運行程式碼,計算值,並且將值填入記憶體位址;
Construction 建置階段
-
loader
負責對模組進行尋址及下載。首先我們修改一個入口檔,這在HTML
中通常是一個<script type="module"></script>
的標籤來表示模組檔。
- 模組繼續透過
import
語句宣告,在import
宣告語句中有一個模組宣告標識符(ModuleSpecifier
),這告訴loader
怎麼找出下一個模組的位址。
- 每一個模組標識號對應一個
模組記錄(Module Record)
,而每一個模組記錄
包含了JavaScript程式碼
、執行上下文
、ImportEntries
、LocalExportEntries
、IndirectExportEntries
、StarExportEntries
。其中ImportEntries
值是一個ImportEntry Records
類型,而LocalExportEntries
、IndirectExportEntries
、StarExportEntries
是一個ExportEntry Records
類型。
ImportEntry Records
- 一個
ImportEntry Records
包含三個欄位ModuleRequest
、ImportName
、LocalName
;
- ModuleRequest: 一個模組識別碼(
ModuleSpecifier
); - ImportName: 由
ModuleRequest
模組識別碼的模組導出所需綁定的名稱。值namespace-object
表示導入請求是針對目標模組的命名空間物件的; - #LocalName: 用於從導入模組中從目前模組存取導入值的變數;
- 詳情可參考下圖:
- 下面這張表記錄了使用
import
匯入的ImportEntry Records
欄位的實例:
導入宣告(Import Statement From) | 模組識別碼(ModuleRequest) | 導入名(ImportName) | 本地名(LocalName) |
---|---|---|---|
#import React from "react"; | "react" | #"default" | "React" |
import * as Moment from "react"; | "react" | namespace -obj | "Moment" |
import {useEffect} from "react"; | "react" | "useEffect" | "useEffect" |
import {useEffect as effect } from "react"; | "react" | #"useEffect" | "effect" |
ExportEntry Records
- 一個
ExportEntry Records
包含四個欄位ExportName
、ModuleRequest
、ImportName
、LocalName
,和ImportEntry Records
不同的是多了一個ExportName
。
- ExportName: 此模組用於匯出時綁定的名稱。
-
下面這張表記錄了使用
export
匯出的ExportEntry Records
欄位的實例:匯出聲明 匯出名 #模組識別碼 #匯入名 本機名稱 export var v; #null null "v" export default function f() {} null null "f" export default function () {}"default" null " default" export default 42;"default" null #" default" export {x}; null null "x" export {v as x}; null null "v" export {x} from "mod"; "mod" "x" null export {v as x} from "mod"; "mod" "v" null #export * from "mod"; "mod" all-but-default null #export * as ns from "mod"; 回到主題
只有當解析完當前的
Module Record
之後,才能知道當前模組依賴的是那些子模組,然後你需要resolve
子模組,取得子模組,再解析子模組,不斷的循環這個流程resolving -> fetching -> parsing,結果如下圖所示:
- 這個過程也稱為
靜態分析
,不會執行JavaScript程式碼,只會辨識export
和import
關鍵字,所以說不能在非全域作用域下使用import
,動態導入除外。 - 如果多個檔案同時依賴一個檔案呢,這會不會造成死循環,答案是不會的。
-
loader
使用Module Map
對全域的MOdule Record
進行追蹤、快取這樣就可以保證模組只被fetch
一次,每個全域作用域中會有一個獨立的Module Map。
MOdule Map 是由一個 URL 記錄和一個字串組成的key/value的對應物件。 URL記錄是取得模組的請求URL,字串指示模組的類型(例如。「javascript」)。模組映射的值要么是模組腳本,null(用於表示失敗的獲取),要么是佔位符值“fetching(獲取中)”。
linking 連結階段
- #在所有
Module Record
被解析完後,接下來JS 引擎需要把所有模組進行連結。 JS 引擎以入口檔案的Module Record
作為起點,以深度優先的順序去遞歸連結模組,為每個Module Record
建立一個Module Environment Record
,用於管理Module Record
中的變數。
-
Module Environment Record
中有一個Binding
,這個是用來存放#Module Record
導出的變數,如上圖所示,在該模組main.js
處導出了一個count
的變數,在Module Environment Record
中的Binding
就會有一個count
,在這個時候,就相當於V8
的編譯階段,創建一個模組實例物件,添加相對應的屬性和方法,此時值為undefined
或null
,為其分配記憶體空間。 - 而在子模組
count.js
中使用了import
關鍵字對main.js
進行導入,而count. js
的import
和main.js
的export
的變數指向的記憶體位置是一致的,這樣就把父子模組之間的關係鏈接起來了。如下圖所示:
- 需要注意的是,我们称
export
导出的为父模块,import
引入的为子模块,父模块可以对变量进行修改,具有读写权限,而子模块只有读权限。
Evaluation 求值阶段
- 在模块彼此链接完之后,执行对应模块文件中顶层作用域的代码,确定链接阶段中定义变量的值,放入内存中。
Es module 是如何解决循环引用的
- 在
Es Module
中有5种状态,分别为unlinked
、linking
、linked
、evaluating
和evaluated
,用循环模块记录(Cyclic Module Records
)的Status
字段来表示,正是通过这个字段来判断模块是否被执行过,每个模块只执行一次。这也是为什么会使用Module Map
来进行全局缓存Module Record
的原因了,如果一个模块的状态为evaluated
,那么下次执行则会自动跳过,从而包装一个模块只会执行一次。Es Module
采用深度优先
的方法对模块图进行遍历,每个模块只执行一次,这也就避免了死循环的情况了。
深度优先搜索算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。
- 看下面的例子,所有的模块只会运行一次:
// main.js import { bar } from "./bar.js"; export const main = "main"; console.log("main"); // foo.js import { main } from "./main.js"; export const foo = "foo"; console.log("foo"); // bar.js import { foo } from "./foo.js"; export const bar = "bar"; console.log("bar");
- 通过
node
运行main.js
,得出以下结果:
- 好了,这篇文章到这也就结束了。《JavaScript视频教程》
以上是一文徹底搞定es6模組化的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

在ES6中,可以利用數組物件的reverse()方法來實現數組反轉,該方法用於顛倒數組中元素的順序,將最後一個元素放在第一位,而第一個元素放在最後,語法「array.reverse()」。 reverse()方法會修改原始數組,如果不想修改需要配合擴充運算子「...」使用,語法「[...array].reverse()」。

async是es7的。 async和await是ES7中新增內容,是對於非同步操作的解決方案;async/await可以說是co模組和生成器函數的語法糖,用更清晰的語意解決js非同步程式碼。 async顧名思義是「非同步」的意思,async用於聲明一個函數是異步的;async和await有一個嚴格規定,兩者都離不開對方,且await只能寫在async函數中。

為了瀏覽器相容。 ES6作為JS的新規範,加入了許多新的語法和API,但現代瀏覽器對ES6新特性支援不高,所以需將ES6程式碼轉換為ES5程式碼。在微信web開發者工具中,會預設使用babel將開發者ES6語法程式碼轉換為三端都能很好支援的ES5的程式碼,幫助開發者解決環境不同所帶來的開發問題;只需要在專案中配置勾選好「ES6轉ES5」選項即可。

步驟:1、將兩個陣列分別轉換為set類型,語法「newA=new Set(a);newB=new Set(b);」;2、利用has()和filter()求差集,語法“ new Set([...newA].filter(x =>!newB.has(x)))”,差集元素會被包含在一個set集合中返回;3、利用Array.from將集合轉為數組類型,語法“Array.from(集合)”。

es5中可以利用for語句和indexOf()函數來實現數組去重,語法“for(i=0;i<數組長度;i++){a=newArr.indexOf(arr[i]);if(a== -1){...}}」。在es6中可以利用擴充運算子、Array.from()和Set來去重;需要先將陣列轉為Set物件來去重,然後利用擴充運算子或Array.from()函數來將Set物件轉回數組即可。

在es6中,暫時性死區是語法錯誤,是指let和const命令使區塊形成封閉的作用域。在程式碼區塊內,使用let/const指令宣告變數之前,變數都是不可用的,在變數宣告之前屬於該變數的「死區」;這在語法上,稱為「暫時性死區」。 ES6規定暫時性死區和let、const語句不出現變量提升,主要是為了減少運行時錯誤,防止在變量聲明前就使用這個變量,從而導致意料之外的行為。

不是,require是CommonJS規範的模組化語法;而es6規範的模組化語法是import。 require是運行時加載,import是編譯時加載;require可以寫在程式碼的任意位置,import只能寫在文件的最頂端且不可在條件語句或函數作用域中使用;require運行時才引入模組的屬性所以效能相對較低,import編譯時引入模組的屬性所所以效能稍高。

在es6中,可以利用array物件的length屬性來判斷數組裡總共有多少項,即取得數組中元素的個數;該屬性可傳回數組中元素的數組,只需要使用「array.length」語句即可傳回表示數組物件的元素個數的數值,也就是長度值。
