ECMA Script 6_模組載入方案 ES6 Module 模組語法_import_export
1. 模組載入方案 commonJS
背景:
歷史上,JavaScript 一直沒有模組(module)體系,
無法將一個大程式拆分成互相依賴的小檔案,再用簡單的方法拼裝起來。
其他語言都有這項功能:
Ruby 的require
Python 的import
甚至就連 CSS 都有@import
但是 JavaScript 任何這方面的支援都沒有,這對開發大型的、複雜的專案形成了巨大障礙
在 ES6 之前,社群制定了一些模組載入方案,最主要的有:
CommonJS 用於伺服器
AMD
ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規
範,成為瀏覽器和伺服器通用的模組解決方案
ES6 模組的設計思想: 是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。
CommonJS 和 AMD 模組,都只能在執行時確定這些東西。
比如,CommonJS 模組就是物件,輸入時必須查詢物件屬性。
執行時載入:實質是整體載入fs
模組(即載入fs
的所有方法),生成一個物件(_fs
),然後再從這個物件上面讀取 3 個方法
-
let { stat, exists, readFile } = require('fs'); //
ES6 模組 不是物件,而是通過 export
命令顯式指定 輸出的程式碼,再通過 import
命令輸入
編譯時載入: 實質是從fs
模組載入 3 個方法,其他方法不載入。
-
import { stat, exists, readFile } from 'fs'; //
ES6 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。
當然,這也導致了沒法引用 ES6 模組本身,因為它不是物件
- ES6 的模組自動採用嚴格模式,不管你有沒有在模組頭部加上
"use strict";
- 限制
-
變數必須聲明後再使用 函式的引數不能有同名屬性,否則報錯 不能使用 with 語句 不能對只讀屬性賦值,否則報錯 不能使用字首 0 表示八進位制數,否則報錯 不能刪除不可刪除的屬性,否則報錯 不能刪除變數 delete prop,會報錯,只能刪除屬性 delete global[prop] eval 不會在它的外層作用域引入變數 eval 和 arguments 不能被重新賦值 arguments不會自動反映函式引數的變化 不能使用 arguments.callee 不能使用 arguments.caller 禁止 this 指向全域性物件 不能使用 fn.caller 和 fn.arguments 獲取函式呼叫的堆疊 增加了保留字(比如 protected、static 和 interface)
2. 模組功能主要由兩個命令構成:export
和 import
export、import
命令 可以出現在模組的任何位置,
只要處於模組頂層就可以,
不能處於塊級作用域內,否則就會報錯
export
用於輸出模組的對外介面
一個模組就是一個獨立的檔案。
注意1. export
語句輸出的介面,與其對應的值是動態繫結關係,
即通過該介面,可以取到模組內部實時的值
-
export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); // 輸出變數 foo,值為bar,500 毫秒之後變成baz
不同於CommonJS 模組輸出的是值的快取,不存在動態更新
注意2. export
命令規定的是對外的介面,必須與模組內部的變數建立一一對應關係。
-
// 報錯 export 1; // 報錯 var m = 1; export m;
// 報錯
function f() {}
export f;/**** 正確寫法 ****/ // 寫法一 export var m = 1; // 寫法二 var m = 1; export {m}; // 寫法三 var n = 1; export {n as m};
// 正確
export function f() {};// 正確
function f() {}
export {f}; export
命令輸出變數
模組檔案內部的所有變數,外部無法獲取。
如果你希望外部能夠讀取模組內部的某個變數,就必須使用 export
關鍵字輸出該變數
-
// profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958;
優先考慮以下寫法。因為這樣就可以在指令碼尾部,一眼看清楚輸出了哪些變數。
-
// profile.js var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export {firstName, lastName, year};
export
命令輸出函式或類(class)-
export function multiply(x, y) { return x * y; };
- 可以使用 export { ...
as...}
關鍵字重新命名 -
function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion // v2 可以用不同的名字輸出兩次。 };
import
用於輸入其他模組提供的功能
其他 JS 檔案就可以通過 import
命令載入這個模組
-
// main.js import {firstName, lastName, year} from './profile.js'; function setName(element) { element.textContent = firstName + ' ' + lastName; }
import
命令要使用as
關鍵字,將輸入的變數重新命名-
import { lastName as surname } from './profile.js';
import
命令輸入的變數都是隻讀的
因為它的本質是輸入介面。
也就是說,不允許在載入模組的腳本里面,改寫介面
-
import {a} from './xxx.js' a = {}; // Syntax Error : 'a' is read-only;
// 如果a是一個物件,改寫a的屬性是允許的
a.foo = 'hello'; // 合法操作 import
命令具有提升效果,會提升到整個模組的頭部,首先執行
本質是,import
命令是編譯階段執行的,在程式碼執行之前就輸入完成了。
- 由於
import
是靜態執行,所以不能使用表示式和變數,這些只有在執行時才能得到結果的語法結構 - 僅僅執行模組,但是不輸入任何值
-
import 'lodash'; import 'lodash'; // 多次重複執行同一句 import 語句,那麼只會執行一次,而不會執行多次
- CommonJS 模組的
require
命令 和 ES6 模組的import
命令,可以寫在同一個模組裡面,但是最好不要這樣做 - 因為
import
在靜態解析階段執行,所以它是一個模組之中最早執行的。下面的程式碼可能不會得到預期結果。 -
require('core-js/modules/es6.symbol'); require('core-js/modules/es6.promise'); import React from 'React';
模組的整體載入
- 現有模組 circle.js
-
// circle.js
export function area(radius) { return Math.PI * radius * radius; }; export function circumference(radius) { return 2 * Math.PI * radius; }; - index.js 整體載入
-
// index.js import * as circle from './circle'; console.log('圓面積:' + circle.area(4)); console.log('圓周長:' + circle.circumference(14));
export default 模組指定預設輸出
使用import
命令的時候,使用者需要知道所要載入的變數名或函式名,否則無法載入。
但是,使用者肯定希望快速上手,未必願意閱讀文件,去了解模組有哪些屬性和方法
為了給使用者提供方便,讓他們不用閱讀文件就能載入模組,就要用到 export default
命令,為模組指定預設輸出
一個模組只能有一個預設輸出, 因此 export default
命令只能使用一次
使用 export default
時,對應的 import
語句不需要使用大括號
-
// export-default.js export default function foo() { console.log('foo'); }; // 或者寫成 function foo() { console.log('foo'); }; export default foo;
- 如果想在一條
import
語句中,同時輸入預設方法和其他介面,可以寫成下面這樣 -
export default function (obj) { // ··· } export function each(obj, iterator, context) { // ··· } export { each as forEach }; /**** 匯入 ****/ import _, { each, forEach } from 'lodash';
跨模組常量 const
引入import()
函式,完成動態載入
import
函式的引數specifier
,指定所要載入的模組的位置。
import
命令能夠接受什麼引數,import()
函式就能接受什麼引數,兩者區別主要是後者為動態載入。
import()
類似於 Node 的require
方法,區別主要是前者是非同步載入,後者是同步載入
import()
返回一個 Promise 物件
-
const main = document.querySelector('main'); import(`./section-modules/${someVariable}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContent = err.message; });
3. 瀏覽器載入
預設情況下,瀏覽器是同步載入 JavaScript 指令碼,即渲染引擎遇到<script>
標籤就會停下
來,等到執行完指令碼,再繼續向下渲染。如果是外部指令碼,還必須加入指令碼下載的時間
如果指令碼體積很大,下載和執行的時間就會很長,因此造成瀏覽器堵塞,使用者會感覺到瀏覽器“卡死”
了,沒有任何響應。這顯然是很不好的體驗,所以瀏覽器允許指令碼非同步載入,
下面就是兩種非同步載入的語法
-
<script src="path/to/myModule.js" defer></script> <script src="path/to/myModule.js" async></script>
<script>
標籤開啟defer
或async
屬性,指令碼就會非同步載入。
渲染引擎遇到這一行命令,就會開始下載外部指令碼,但不會等它下載和執行,而是直接執行後面的命令。
defer
與 async
的區別是:
defer
要等到整個頁面在記憶體中正常渲染結束
(DOM 結構完全生成,以及其他指令碼執行完成),才會執行
async
一旦下載完,渲染引擎就會中斷渲染,
執行這個指令碼以後,再繼續渲染
- 瀏覽器載入 ES6 模組,也使用
<script>
標籤,但是要加入type="module"
屬性 -
<script type="module" src="./foo.js"></script>
瀏覽器對於帶有 type="module"
的 <script>
,都是非同步載入,不會造成堵塞瀏覽器,
即等到整個頁面渲染完,再執行模組指令碼,等同於打開了 <script>
標籤的 defer
屬性。
ES6 模組也允許內嵌在網頁中,語法行為與載入外部指令碼完全一致
-
<script type="module"> import utils from "./utils.js"; // other code </script>
注意:
-
-
-
-
-
- 程式碼是在模組作用域之中執行,而不是在全域性作用域執行。模組內部的頂層變數,外部不可見。
- 模組指令碼自動採用嚴格模式,不管有沒有宣告
use strict
。 - 模組之中,可以使用
import
命令載入其他模組(.js
字尾不可省略,需要提供絕對 URL 或相對 URL),也可以使用export
命令輸出對外介面。 - 模組之中,頂層的
this
關鍵字返回undefined
,而不是指向window
。也就是說,在模組頂層使用this
關鍵字,是無意義的。 - 同一個模組如果載入多次,將只執行一次。
-
-
-
-
- 利用頂層的
this
等於undefined
這個語法點,可以偵測當前程式碼是否在 ES6 模組之中。 -
const isNotModuleScript = this !== undefined
4. ES6 模組與 CommonJS 模組完全不同。
- CommonJS 模組輸出的是值的拷貝 ES6 模組輸出的是值的引用。
- CommonJS 模組是執行時載入 ES6 模組是編譯時輸出介面。
CommonJS 載入的是一個物件(即module.exports
屬性),該物件只有在指令碼執行完才會生成
ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成
- CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值
-
/**** 定義介面 lib.js ****/ var counter = 3; function incCounter() { counter++; }; module.exports = { counter: counter, incCounter: incCounter, }; /**** 匯入 main.js ****/ var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); // 改變的是模組檔案中的值,而當前檔案的值不受影響 console.log(mod.counter); // 3
- ES6 模組的執行機制與 CommonJS 不一樣。
JS 引擎對指令碼靜態分析的時候,遇到模組載入命令import
,就會生成一個只讀引用。
等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值
-
// lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 第一次取值 incCounter(); console.log(counter); // 4 再取值,發現值變了
- 唯一要注意的是: ES6 輸入的模組變數,只是一個“符號連線”,所以這個變數是隻讀的,對它進行重新賦值會報錯
5. Node 對 ES6 模組的處理比較麻煩,因為它有自己的 CommonJS 模組格式,與 ES6 模組格式是不相容的。
目前的解決方案是,將兩者分開,ES6 模組 和 CommonJS 採用各自的載入方案
- 為了與瀏覽器的
import
載入規則相同,Node 的.mjs
檔案支援 URL 路徑。 -
import './foo?query=1'; // 載入 ./foo 傳入引數 ?query=1
- 只要檔名中含有
:
、%
、#
、?
等特殊字元,最好對這些字元進行轉義。
因為 Node 會按 URL 規則解讀
- Node 的
import
命令只支援載入本地模組(file:
協議),不支援載入遠端模組 - 如果模組名不含路徑,那麼
import
命令會去node_modules
目錄尋找這個模組。 - 如果指令碼檔案省略了字尾名,
比如import './foo'
,Node 會依次嘗試四個字尾名
./foo.mjs
./foo.js
./foo.json
./foo.node
。
如果這些指令碼檔案都不存在,Node 就會去載入 ./foo/package.json
的 main
欄位指定的指令碼。
如果 ./foo/package.json
不存在 或者 沒有 main
欄位,那麼就會丟擲錯誤。
6. ES6 模組載入 CommonJS 模組
CommonJS 模組的輸出 都定義在 module.exports
這個屬性上面
-
// a.js module.exports = { foo: 'hello', bar: 'world' }; // 等同於 export default { foo: 'hello', bar: 'world' }; /**** export 指向 modeule.exports, 即 exports 變數 是對 module 的 exports 屬性的引用 因此 ****/ module.exports = func; // 正確 export = func; // 錯誤
module.exports
會被視為預設輸出,即import
命令實際上輸入的是這樣一個物件{ default: module.exports }
- 通過 import 一共有三種寫法,可以拿到 CommonJS 模組的
module.exports
-
// 寫法一 import baz from './a'; // baz = {foo: 'hello', bar: 'world'}; // 寫法二 import {default as baz} from './a'; // baz = {foo: 'hello', bar: 'world'}; // 寫法三 import * as baz from './a'; // baz = { // get default() {return module.exports;}, // get foo() {return this.default.foo}.bind(baz), // get bar() {return this.default.bar}.bind(baz) // }
- CommonJS 的一個模組,就是一個指令碼檔案。
- 可以通過將變數和函式設定為 module.exports / exports 的屬性來暴露變數和函式
require
命令第一次載入該指令碼,就會執行整個指令碼,然後在記憶體生成一個物件