ES6 Module 的加載實現(xiàn)

2020-06-11 15:03 更新

1. 瀏覽器加載

傳統(tǒng)方法

HTML 網(wǎng)頁中,瀏覽器通過<script>標(biāo)簽加載 JavaScript 腳本。

<!-- 頁面內(nèi)嵌的腳本 -->
<script type="application/javascript">
  // module code
</script>


<!-- 外部腳本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

上面代碼中,由于瀏覽器腳本的默認(rèn)語言是 JavaScript,因此 type="application/javascript"可以省略。

默認(rèn)情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到 標(biāo)簽就會停下來,等到執(zhí)行完腳本,再繼續(xù)向下渲染。如果是外部腳本,還必須加入腳本下載的時間。

如果腳本體積很大,下載和執(zhí)行的時間就會很長,因此造成瀏覽器堵塞,用戶會感覺到瀏覽器“卡死”了,沒有任何響應(yīng)。這顯然是很不好的體驗,所以瀏覽器允許腳本異步加載,下面就是兩種異步加載的語法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

上面代碼中,<script>標(biāo)簽打開 deferasync 屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執(zhí)行,而是直接執(zhí)行后面的命令。

deferasync 的區(qū)別是: defer要等到整個頁面在內(nèi)存中正常渲染結(jié)束(DOM 結(jié)構(gòu)完全生成,以及其他腳本執(zhí)行完成),才會執(zhí)行;async一旦下載完,渲染引擎就會中斷渲染,執(zhí)行這個腳本以后,再繼續(xù)渲染。一句話, defer是“渲染完再執(zhí)行”, async 是“下載完就執(zhí)行”。另外,如果有多個 defer 腳本,會按照它們在頁面出現(xiàn)的順序加載,而多個async 腳本是不能保證加載順序的。

加載規(guī)則

瀏覽器加載 ES6 模塊,也使用<script>標(biāo)簽,但是要加入type="module" 屬性。

<script type="module" src="./foo.js"></script>

上面代碼在網(wǎng)頁中插入一個模塊foo.js ,由于 type 屬性設(shè)為 module,所以瀏覽器知道這是一個 ES6 模塊。

瀏覽器對于帶有 type="module"的 ,都是異步加載,不會造成堵塞瀏覽器,即等到整個頁面渲染完,再執(zhí)行模塊腳本,等同于打開了<script>標(biāo)簽的 defer屬性。

<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

如果網(wǎng)頁有多個<script type="module"> ,它們會按照在頁面出現(xiàn)的順序依次執(zhí)行。

<script>標(biāo)簽的async屬性也可以打開,這時只要加載完成,渲染引擎就會中斷渲染立即執(zhí)行。執(zhí)行完成后,再恢復(fù)渲染。

<script type="module" src="./foo.js" async></script>

一旦使用了async 屬性,<script type="module">就不會按照在頁面出現(xiàn)的順序執(zhí)行,而是只要該模塊加載完成,就執(zhí)行該模塊。

ES6 模塊也允許內(nèi)嵌在網(wǎng)頁中,語法行為與加載外部腳本完全一致。

<script type="module">
  import utils from "./utils.js";


  // other code
</script>

舉例來說,jQuery 就支持模塊加載。

<script type="module">
  import $ from "./jquery/src/jquery.js";
  $('#message').text('Hi from jQuery!');
</script>

對于外部的模塊腳本(上例是 foo.js),有幾點需要注意。

  • 代碼是在模塊作用域之中運行,而不是在全局作用域運行。模塊內(nèi)部的頂層變量,外部不可見。
  • 模塊腳本自動采用嚴(yán)格模式,不管有沒有聲明 use strict。
  • 模塊之中,可以使用 import命令加載其他模塊( .js 后綴不可省略,需要提供絕對 URL 或相對 URL),也可以使用 export 命令輸出對外接口。
  • 模塊之中,頂層的 this 關(guān)鍵字返回 undefined,而不是指向 window 。也就是說,在模塊頂層使用this關(guān)鍵字,是無意義的。
  • 同一個模塊如果加載多次,將只執(zhí)行一次。

下面是一個示例模塊。

import utils from 'https://example.com/js/utils.js';


const x = 1;


console.log(x === window.x); //false
console.log(this === undefined); // true

利用頂層的 this 等于 undefined這個語法點,可以偵測當(dāng)前代碼是否在 ES6 模塊之中。

const isNotModuleScript = this !== undefined;

2. ES6 模塊與 CommonJS 模塊的差異

討論 Node.js 加載 ES6 模塊之前,必須了解 ES6 模塊與 CommonJS 模塊完全不同。

它們有兩個重大差異。

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

第二個差異是因為 CommonJS 加載的是一個對象(即 module.exports 屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會生成。

下面重點解釋第一個差異。

CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值。請看下面這個模塊文件 lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面代碼輸出內(nèi)部變量 counter 和改寫這個變量的內(nèi)部方法 incCounter。然后,在main.js 里面加載這個模塊。

// main.js
var mod = require('./lib');


console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上面代碼說明,lib.js模塊加載以后,它的內(nèi)部變化就影響不到輸出的mod.counter了。這是因為 mod.counter 是一個原始類型的值,會被緩存。除非寫成一個函數(shù),才能得到內(nèi)部變動后的值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面代碼中,輸出的 counter 屬性實際上是一個取值器函數(shù)?,F(xiàn)在再執(zhí)行 main.js ,就可以正確讀取內(nèi)部變量counter的變動了。

$ node main.js
3
4

ES6 模塊的運行機(jī)制與 CommonJS 不一樣。JS 引擎對腳本靜態(tài)分析的時候,遇到模塊加載命令import ,就會生成一個只讀引用。等到腳本真正執(zhí)行時,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的import有點像 Unix 系統(tǒng)的“符號連接”,原始值變了,import 加載的值也會跟著變。因此,ES6 模塊是動態(tài)引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。

還是舉上面的例子。

// 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 模塊輸入的變量counter 是活的,完全反應(yīng)其所在模塊lib.js內(nèi)部的變化。

再舉一個出現(xiàn)在export一節(jié)中的例子。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);


// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代碼中, m1.js 的變量foo,在剛加載時等于 bar,過了 500 毫秒,又變?yōu)榈扔?baz 。`

讓我們看看, m2.js 能否正確讀取這個變化。

$ babel-node m2.js


bar
baz

上面代碼表明,ES6 模塊不會緩存運行結(jié)果,而是動態(tài)地去被加載的模塊取值,并且變量總是綁定其所在的模塊。

由于 ES6 輸入的模塊變量,只是一個“符號連接”,所以這個變量是只讀的,對它進(jìn)行重新賦值會報錯。

// lib.js
export let obj = {};


// main.js
import { obj } from './lib';


obj.prop = 123; // OK
obj = {}; // TypeError

上面代碼中, main.js 從 lib.js 輸入變量 obj ,可以對 obj 添加屬性,但是重新賦值就會報錯。因為變量 obj 指向的地址是只讀的,不能重新賦值,這就好比 main.js 創(chuàng)造了一個名為 obj 的 const 變量。

最后, export 通過接口,輸出的是同一個值。不同的腳本加載這個接口,得到的都是同樣的實例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}


export let c = new C();

上面的腳本 mod.js ,輸出的是一個 C 的實例。不同的腳本加載這個模塊,得到的都是同一個實例。

// x.js
import {c} from './mod';
c.add();


// y.js
import {c} from './mod';
c.show();


// main.js
import './x';
import './y';

現(xiàn)在執(zhí)行 main.js ,輸出的是 1 。

$ babel-node main.js
1

這就證明了 x.js 和 y.js 加載的都是 C 的同一個實例。

3. Node.js 加載

概述

Node.js 對 ES6 模塊的處理比較麻煩,因為它有自己的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將兩者分開,ES6 模塊和 CommonJS 采用各自的加載方案。從 v13.2 版本開始,Node.js 已經(jīng)默認(rèn)打開了 ES6 模塊支持。

Node.js 要求 ES6 模塊采用 .mjs 后綴文件名。也就是說,只要腳本文件里面使用 import或者 export 命令,那么就必須采用 .mjs后綴名。Node.js 遇到 .mjs文件,就認(rèn)為它是 ES6 模塊,默認(rèn)啟用嚴(yán)格模式,不必在每個模塊文件頂部指定 "use strict"

如果不希望將后綴名改成 .mjs ,可以在項目的 package.json 文件中,指定 type 字段為 module 。

{
   "type": "module"
}

一旦設(shè)置了以后,該目錄里面的 JS 腳本,就被解釋用 ES6 模塊。

## 解釋成 ES6 模塊
$ node my-app.js

如果這時還要使用 CommonJS 模塊,那么需要將 CommonJS 腳本的后綴名都改成 .cjs 。如果沒有 type 字段,或者 type 字段為 commonjs ,則 .js 腳本會被解釋成 CommonJS 模塊。

總結(jié)為一句話: .mjs 文件總是以 ES6 模塊加載, .cjs 文件總是以 CommonJS 模塊加載, .js 文件的加載取決于 package.json 里面 type 字段的設(shè)置。

注意,ES6 模塊與 CommonJS 模塊盡量不要混用。 require 命令不能加載 .mjs 文件,會報錯,只有 import 命令才可以加載 .mjs 文件。反過來, .mjs 文件里面也不能使用 require 命令,必須使用 import 。

main 字段

package.json文件有兩個字段可以指定模塊的入口文件:mainexports 。比較簡單的模塊,可以只使用main 字段,指定模塊加載的入口文件。

// ./node_modules/es-module-package/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

上面代碼指定項目的入口腳本為 ./src/index.js ,它的格式為 ES6 模塊。如果沒有 type 字段, index.js 就會被解釋為 CommonJS 模塊。

然后, import 命令就可以加載這個模塊。

// ./my-app.mjs


import { something } from 'es-module-package';
// 實際加載的是 ./node_modules/es-module-package/src/index.js

上面代碼中,運行該腳本以后,Node.js 就會到 ./node_modules 目錄下面,尋找 es-module-package 模塊,然后根據(jù)該模塊 package.json 的 main 字段去執(zhí)行入口文件。

這時,如果用 CommonJS 模塊的 require() 命令去加載 es-module-package 模塊會報錯,因為 CommonJS 模塊不能處理 export 命令。

exports 字段

exports字段的優(yōu)先級高于main字段。它有多種用法。

(1)子目錄別名

package.json 文件的 exports 字段可以指定腳本或子目錄的別名。

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./submodule": "./src/submodule.js"
  }
}

上面的代碼指定 src/submodule.js 別名為 submodule ,然后就可以從別名加載這個文件。

import submodule from 'es-module-package/submodule';
// 加載 ./node_modules/es-module-package/src/submodule.js

下面是子目錄別名的例子。

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./features/": "./src/features/"
  }
}


import feature from 'es-module-package/features/x.js';
// 加載 ./node_modules/es-module-package/src/features/x.js

如果沒有指定別名,就不能用“模塊+腳本名”這種形式加載腳本。

// 報錯
import submodule from 'es-module-package/private-module.js';


// 不報錯
import submodule from './node_modules/es-module-package/private-module.js';

(2)main 的別名

exports 字段的別名如果是 . ,就代表模塊的主入口,優(yōu)先級高于 main 字段,并且可以直接簡寫成 exports 字段的值。

{
  "exports": {
    ".": "./main.js"
  }
}


// 等同于
{
  "exports": "./main.js"
}

由于 exports 字段只有支持 ES6 的 Node.js 才認(rèn)識,所以可以用來兼容舊版本的 Node.js。

{
  "main": "./main-legacy.cjs",
  "exports": {
    ".": "./main-modern.cjs"
  }
}

上面代碼中,老版本的 Node.js (不支持 ES6 模塊)的入口文件是 main-legacy.cjs ,新版本的 Node.js 的入口文件是 main-modern.cjs 。

(3)條件加載

利用 . 這個別名,可以為 ES6 模塊和 CommonJS 指定不同的入口。目前,這個功能需要在 Node.js 運行的時候,打開 --experimental-conditional-exports 標(biāo)志。

{
  "type": "module",
  "exports": {
    ".": {
      "require": "./main.cjs",
      "default": "./main.js"
    }
  }
}

上面代碼中,別名 . 的 require 條件指定 require() 命令的入口文件(即 CommonJS 的入口), default 條件指定其他情況的入口(即 ES6 的入口)。

上面的寫法可以簡寫如下。

{
  "exports": {
    "require": "./main.cjs",
    "default": "./main.js"
  }
}

注意,如果同時還有其他別名,就不能采用簡寫,否則或報錯。

{
  // 報錯
  "exports": {
    "./feature": "./lib/feature.js",
    "require": "./main.cjs",
    "default": "./main.js"
  }
}

ES6 模塊加載 CommonJS 模塊

目前,一個模塊同時支持 ES6 和 CommonJS 兩種格式的常見方法是,package.json文件的 main字段指定 CommonJS 入口,給 Node.js 使用; module 字段指定 ES6 模塊入口,給打包工具使用,因為 Node.js 不認(rèn)識 module 字段。

有了上一節(jié)的條件加載以后,Node.js 本身就可以同時處理兩種模塊。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "main": "./index.cjs",
  "exports": {
    "require": "./index.cjs",
    "default": "./wrapper.mjs"
  }
}

上面代碼指定了 CommonJS 入口文件 index.cjs ,下面是這個文件的代碼。

// ./node_modules/pkg/index.cjs
exports.name = 'value';

然后,ES6 模塊可以加載這個文件。

// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;

注意, import 命令加載 CommonJS 模塊,只能整體加載,不能只加載單一的輸出項。

// 正確
import packageMain from 'commonjs-package';


// 報錯
import { method } from 'commonjs-package';

還有一種變通的加載方法,就是使用 Node.js 內(nèi)置的 module.createRequire() 方法。

// cjs.cjs
module.exports = 'cjs';


// esm.mjs
import { createRequire } from 'module';


const require = createRequire(import.meta.url);


const cjs = require('./cjs.cjs');
cjs === 'cjs'; // true

上面代碼中,ES6 模塊通過 module.createRequire() 方法可以加載 CommonJS 模塊

CommonJS 模塊加載 ES6 模塊

CommonJSrequire 命令不能加載ES6模塊,會報錯,只能使用 import()這個方法加載。

(async () => {
  await import('./my-app.mjs');
})();

上面代碼可以在 CommonJS 模塊中運行。

Node.js 的內(nèi)置模塊

Node.js 的內(nèi)置模塊可以整體加載,也可以加載指定的輸出項。

// 整體加載
import EventEmitter from 'events';
const e = new EventEmitter();


// 加載指定的輸出項
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {
  if (err) {
    console.error(err);
  } else {
    console.log(source);
  }
});

加載路徑

ES6 模塊的加載路徑必須給出腳本的完整路徑,不能省略腳本的后綴名。 import命令和 package.json 文件的 main 字段如果省略腳本的后綴名,會報錯。

// ES6 模塊中將報錯
import { something } from './index';

為了與瀏覽器的import 加載規(guī)則相同,Node.js 的 .mjs文件支持 URL 路徑。

import './foo.mjs?query=1'; // 加載 ./foo 傳入?yún)?shù) ?query=1

上面代碼中,腳本路徑帶有參數(shù) ?query=1 ,Node 會按 URL 規(guī)則解讀。同一個腳本只要參數(shù)不同,就會被加載多次,并且保存成不同的緩存。由于這個原因,只要文件名中含有 : 、 % 、 # 、 ? 等特殊字符,最好對這些字符進(jìn)行轉(zhuǎn)義。

目前,Node.js 的 import 命令只支持加載本地模塊( file: 協(xié)議)和 data: 協(xié)議,不支持加載遠(yuǎn)程模塊。另外,腳本路徑只支持相對路徑,不支持絕對路徑(即以 / 或 // 開頭的路徑)。

最后,Node 的` import ``命令是異步加載,這一點與瀏覽器的處理方法相同。

內(nèi)部變量

ES6 模塊應(yīng)該是通用的,同一個模塊不用修改,就可以用在瀏覽器環(huán)境和服務(wù)器環(huán)境。為了達(dá)到這個目標(biāo),Node 規(guī)定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內(nèi)部變量。

首先,就是 this 關(guān)鍵字。ES6 模塊之中,頂層的 this 指向 undefined ;CommonJS 模塊的頂層 this 指向當(dāng)前模塊,這是兩者的一個重大差異。

其次,以下這些頂層變量在 ES6 模塊之中都是不存在的。

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

4. 循環(huán)加載

“循環(huán)加載”(circular dependency)指的是, a 腳本的執(zhí)行依賴 b 腳本,而 b 腳本的執(zhí)行又依賴 a 腳本。

// a.js
var b = require('b');


// b.js
var a = require('a');

通常,“循環(huán)加載”表示存在強(qiáng)耦合,如果處理不好,還可能導(dǎo)致遞歸加載,使得程序無法執(zhí)行,因此應(yīng)該避免出現(xiàn)。

但是實際上,這是很難避免的,尤其是依賴關(guān)系復(fù)雜的大項目,很容易出現(xiàn) a 依賴 b , b 依賴 c , c 又依賴 a 這樣的情況。這意味著,模塊加載機(jī)制必須考慮“循環(huán)加載”的情況。

對于 JavaScript 語言來說,目前最常見的兩種模塊格式 CommonJS 和 ES6,處理“循環(huán)加載”的方法是不一樣的,返回的結(jié)果也不一樣。

CommonJS 模塊的加載原理

介紹 ES6 如何處理“循環(huán)加載”之前,先介紹目前最流行的 CommonJS 模塊格式的加載原理。

CommonJS 的一個模塊,就是一個腳本文件。 require 命令第一次加載該腳本,就會執(zhí)行整個腳本,然后在內(nèi)存生成一個對象。

{
  id: '...',
  exports: { ... },
  loaded: true,
  ...
}

上面代碼就是 Node 內(nèi)部加載模塊后生成的一個對象。該對象的 id 屬性是模塊名, exports 屬性是模塊輸出的各個接口, loaded 屬性是一個布爾值,表示該模塊的腳本是否執(zhí)行完畢。其他還有很多屬性,這里都省略了。

以后需要用到這個模塊的時候,就會到 exports 屬性上面取值。即使再次執(zhí)行 require 命令,也不會再次執(zhí)行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊無論加載多少次,都只會在第一次加載時運行一次,以后再加載,就返回第一次運行的結(jié)果,除非手動清除系統(tǒng)緩存。

CommonJS 模塊的循環(huán)加載

CommonJS 模塊的重要特性是加載時執(zhí)行,即腳本代碼在require的時候,就會全部執(zhí)行。一旦出現(xiàn)某個模塊被"循環(huán)加載",就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會輸出。

讓我們來看,Node 官方文檔里面的例子。腳本文件 a.js 代碼如下。

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執(zhí)行完畢');

上面代碼之中, a.js 腳本先輸出一個 done 變量,然后加載另一個腳本文件 b.js 。注意,此時 a.js 代碼就停在這里,等待 b.js 執(zhí)行完畢,再往下執(zhí)行。

再看 b.js 的代碼。

exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執(zhí)行完畢');

上面代碼之中, b.js 執(zhí)行到第二行,就會去加載 a.js ,這時,就發(fā)生了“循環(huán)加載”。系統(tǒng)會去 a.js 模塊對應(yīng)對象的 exports 屬性取值,可是因為 a.js 還沒有執(zhí)行完,從 exports 屬性只能取回已經(jīng)執(zhí)行的部分,而不是最后的值。

a.js 已經(jīng)執(zhí)行的部分,只有一行。

exports.done = false;

因此,對于 b.js 來說,它從 a.js 只輸入一個變量 done ,值為 false 。

然后, b.js 接著往下執(zhí)行,等到全部執(zhí)行完畢,再把執(zhí)行權(quán)交還給 a.js 。于是, a.js 接著往下執(zhí)行,直到執(zhí)行完畢。我們寫一個腳本 main.js ,驗證這個過程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

執(zhí)行 main.js ,運行結(jié)果如下。

$ node main.js


在 b.js 之中,a.done = false
b.js 執(zhí)行完畢
在 a.js 之中,b.done = true
a.js 執(zhí)行完畢
在 main.js 之中, a.done=true, b.done=true

上面的代碼證明了兩件事。一是,在 b.js 之中, a.js 沒有執(zhí)行完畢,只執(zhí)行了第一行。二是, main.js 執(zhí)行到第二行時,不會再次執(zhí)行 b.js ,而是輸出緩存的 b.js 的執(zhí)行結(jié)果,即它的第四行。

exports.done = true;

總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。

另外,由于 CommonJS 模塊遇到循環(huán)加載時,返回的是當(dāng)前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值,兩者可能會有差異。所以,輸入變量的時候,必須非常小心。

var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險的寫法


exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};


exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一個部分加載時的值
};

上面代碼中,如果發(fā)生循環(huán)加載, require('a').foo 的值很可能后面會被改寫,改用 require('a') 會更保險一點。

ES6 模塊的循環(huán)加載

ES6 處理“循環(huán)加載”與 CommonJS 有本質(zhì)的不同。ES6 模塊是動態(tài)引用,如果使用 import從一個模塊加載變量(即import foo from 'foo' ),那些變量不會被緩存,而是成為一個指向被加載模塊的引用,需要開發(fā)者自己保證,真正取值的時候能夠取到值。

請看下面這個例子。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';


// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

上面代碼中, a.mjs 加載b.mjs, b.mjs 又加載 a.mjs ,構(gòu)成循環(huán)加載。執(zhí)行 a.mjs ,結(jié)果如下。

$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined

上面代碼中,執(zhí)行 a.mjs 以后會報錯, foo 變量未定義,這是為什么?

讓我們一行行來看,ES6 循環(huán)加載是怎么處理的。首先,執(zhí)行 a.mjs 以后,引擎發(fā)現(xiàn)它加載了 b.mjs ,因此會優(yōu)先執(zhí)行 b.mjs ,然后再執(zhí)行 a.mjs 。接著,執(zhí)行 b.mjs 的時候,已知它從 a.mjs 輸入了 foo 接口,這時不會去執(zhí)行 a.mjs ,而是認(rèn)為這個接口已經(jīng)存在了,繼續(xù)往下執(zhí)行。執(zhí)行到第三行 console.log(foo) 的時候,才發(fā)現(xiàn)這個接口根本沒定義,因此報錯。

解決這個問題的方法,就是讓 b.mjs 運行的時候, foo 已經(jīng)有定義了。這可以通過將 foo 寫成函數(shù)來解決。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};


// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

這時再執(zhí)行 a.mjs 就可以得到預(yù)期結(jié)果。

$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar

這是因為函數(shù)具有提升作用,在執(zhí)行 import {bar} from './b' 時,函數(shù) foo 就已經(jīng)有定義了,所以 b.mjs 加載的時候不會報錯。這也意味著,如果把函數(shù) foo 改寫成函數(shù)表達(dá)式,也會報錯。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};

上面代碼的第四行,改成了函數(shù)表達(dá)式,就不具有提升作用,執(zhí)行就會報錯。

我們再來看 ES6 模塊加載器SystemJS給出的一個例子。

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n === 0 || odd(n - 1);
}


// odd.js
import { even } from './even';
export function odd(n) {
  return n !== 0 && even(n - 1);
}

上面代碼中, even.js 里面的函數(shù) even 有一個參數(shù) n ,只要不等于 0,就會減去 1,傳入加載的 odd() 。 odd.js 也會做類似操作。

運行上面這段代碼,結(jié)果如下。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17

上面代碼中,參數(shù) n 從 10 變?yōu)?0 的過程中, even() 一共會執(zhí)行 6 次,所以變量 counter 等于 6。第二次調(diào)用 even() 時,參數(shù) n 從 20 變?yōu)?0, even() 一共會執(zhí)行 11 次,加上前面的 6 次,所以變量 counter 等于 17。

這個例子要是改寫成 CommonJS,就根本無法執(zhí)行,會報錯。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
  counter++;
  return n == 0 || odd(n - 1);
}


// odd.js
var even = require('./even').even;
module.exports = function (n) {
  return n != 0 && even(n - 1);
}

上面代碼中, even.js 加載 odd.js ,而 odd.js 又去加載 even.js ,形成“循環(huán)加載”。這時,執(zhí)行引擎就會輸出 even.js 已經(jīng)執(zhí)行的部分(不存在任何結(jié)果),所以在 odd.js 之中,變量 even 等于 undefined ,等到后面調(diào)用 even(n - 1) 就會報錯。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號