上一章介紹了模塊的語(yǔ)法,本章介紹如何在瀏覽器和 Node.js 之中加載 ES6 模塊,以及實(shí)際開(kāi)發(fā)中經(jīng)常遇到的一些問(wèn)題(比如循環(huán)加載)。1、瀏覽器加載
傳統(tǒng)方法
HTML 網(wǎng)頁(yè)中,瀏覽器通過(guò)
上面代碼中,由于瀏覽器腳本的默認(rèn)語(yǔ)言是 JavaScript,因此type="application/javascript"可以省略。默認(rèn)情況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到
上面代碼在網(wǎng)頁(yè)中插入一個(gè)模塊foo.js,由于type屬性設(shè)為module,所以瀏覽器知道這是一個(gè) ES6 模塊。瀏覽器對(duì)于帶有type="module"的
舉例來(lái)說(shuō),jQuery 就支持模塊加載。對(duì)于外部的模塊腳本(上例是foo.js),有幾點(diǎn)需要注意。代碼是在模塊作用域之中運(yùn)行,而不是在全局作用域運(yùn)行。模塊內(nèi)部的頂層變量,外部不可見(jiàn)。
模塊腳本自動(dòng)采用嚴(yán)格模式,不管有沒(méi)有聲明use strict。
模塊之中,可以使用import命令加載其他模塊(.js后綴不可省略,需要提供絕對(duì) URL 或相對(duì) URL),也可以使用export命令輸出對(duì)外接口。
模塊之中,頂層的this關(guān)鍵字返回undefined,而不是指向window。也就是說(shuō),在模塊頂層使用this關(guān)鍵字,是無(wú)意義的。
同一個(gè)模塊如果加載多次,將只執(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這個(gè)語(yǔ)法點(diǎn),可以偵測(cè)當(dāng)前代碼是否在 ES6 模塊之中。const isNotModuleScript = this !== undefined;
2、ES6 模塊與 CommonJS 模塊的差異
討論 Node.js 加載 ES6 模塊之前,必須了解 ES6 模塊與 CommonJS 模塊完全不同。CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出接口。
CommonJS 模塊的require()是同步加載模塊,ES6 模塊的import命令是異步加載,有一個(gè)獨(dú)立的模塊依賴的解析階段。
第二個(gè)差異是因?yàn)?CommonJS 加載的是一個(gè)對(duì)象(即module.exports屬性),該對(duì)象只有在腳本運(yùn)行完才會(huì)生成。而 ES6 模塊不是對(duì)象,它的對(duì)外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會(huì)生成。CommonJS 模塊輸出的是值的拷貝,也就是說(shuō),一旦輸出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值。請(qǐng)看下面這個(gè)模塊文件lib.js的例子。// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代碼輸出內(nèi)部變量counter和改寫(xiě)這個(gè)變量的內(nèi)部方法incCounter。然后,在main.js里面加載這個(gè)模塊。// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代碼說(shuō)明,lib.js模塊加載以后,它的內(nèi)部變化就影響不到輸出的mod.counter了。這是因?yàn)閙od.counter是一個(gè)原始類型的值,會(huì)被緩存。除非寫(xiě)成一個(gè)函數(shù),才能得到內(nèi)部變動(dòng)后的值。// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
上面代碼中,輸出的counter屬性實(shí)際上是一個(gè)取值器函數(shù)。現(xiàn)在再執(zhí)行main.js,就可以正確讀取內(nèi)部變量counter的變動(dòng)了。ES6 模塊的運(yùn)行機(jī)制與 CommonJS 不一樣。JS 引擎對(duì)腳本靜態(tài)分析的時(shí)候,遇到模塊加載命令import,就會(huì)生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí),再根據(jù)這個(gè)只讀引用,到被加載的那個(gè)模塊里面去取值。換句話說(shuō),ES6 的import有點(diǎn)像 Unix 系統(tǒng)的“符號(hào)連接”,原始值變了,import加載的值也會(huì)跟著變。因此,ES6 模塊是動(dòng)態(tài)引用,并且不會(huì)緩存值,模塊里面的變量綁定其所在的模塊。// 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
上面代碼說(shuō)明,ES6 模塊輸入的變量counter是活的,完全反應(yīng)其所在模塊lib.js內(nèi)部的變化。再舉一個(gè)出現(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,在剛加載時(shí)等于bar,過(guò)了 500 毫秒,又變?yōu)榈扔赽az。讓我們看看,m2.js能否正確讀取這個(gè)變化。上面代碼表明,ES6 模塊不會(huì)緩存運(yùn)行結(jié)果,而是動(dòng)態(tài)地去被加載的模塊取值,并且變量總是綁定其所在的模塊。由于 ES6 輸入的模塊變量,只是一個(gè)“符號(hào)連接”,所以這個(gè)變量是只讀的,對(duì)它進(jìn)行重新賦值會(huì)報(bào)錯(cuò)。// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
上面代碼中,main.js從lib.js輸入變量obj,可以對(duì)obj添加屬性,但是重新賦值就會(huì)報(bào)錯(cuò)。因?yàn)樽兞縪bj指向的地址是只讀的,不能重新賦值,這就好比main.js創(chuàng)造了一個(gè)名為obj的const變量。最后,export通過(guò)接口,輸出的是同一個(gè)值。不同的腳本加載這個(gè)接口,得到的都是同樣的實(shí)例。// 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,輸出的是一個(gè)C的實(shí)例。不同的腳本加載這個(gè)模塊,得到的都是同一個(gè)實(shí)例。// 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。這就證明了x.js和y.js加載的都是C的同一個(gè)實(shí)例。3、Node.js 的模塊加載方法
概述
JavaScript 現(xiàn)在有兩種模塊。一種是 ES6 模塊,簡(jiǎn)稱 ESM;另一種是 CommonJS 模塊,簡(jiǎn)稱 CJS。CommonJS 模塊是 Node.js 專用的,與 ES6 模塊不兼容。語(yǔ)法上面,兩者最明顯的差異是,CommonJS 模塊使用require()和module.exports,ES6 模塊使用import和export。它們采用不同的加載方案。從 Node.js v13.2 版本開(kāi)始,Node.js 已經(jīng)默認(rèn)打開(kāi)了 ES6 模塊支持。Node.js 要求 ES6 模塊采用.mjs后綴文件名。也就是說(shuō),只要腳本文件里面使用import或者export命令,那么就必須采用.mjs后綴名。Node.js 遇到.mjs文件,就認(rèn)為它是 ES6 模塊,默認(rèn)啟用嚴(yán)格模式,不必在每個(gè)模塊文件頂部指定"use strict"。如果不希望將后綴名改成.mjs,可以在項(xiàng)目的package.json文件中,指定type字段為module。一旦設(shè)置了以后,該目錄里面的 JS 腳本,就被解釋用 ES6 模塊。# 解釋成 ES6 模塊
$ node my-app.js
如果這時(shí)還要使用 CommonJS 模塊,那么需要將 CommonJS 腳本的后綴名都改成.cjs。如果沒(méi)有type字段,或者type字段為commonjs,則.js腳本會(huì)被解釋成 CommonJS 模塊。總結(jié)為一句話:.mjs文件總是以 ES6 模塊加載,.cjs文件總是以 CommonJS 模塊加載,.js文件的加載取決于package.json里面type字段的設(shè)置。注意,ES6 模塊與 CommonJS 模塊盡量不要混用。require命令不能加載.mjs文件,會(huì)報(bào)錯(cuò),只有import命令才可以加載.mjs文件。反過(guò)來(lái),.mjs文件里面也不能使用require命令,必須使用import。package.json 的 main 字段
package.json文件有兩個(gè)字段可以指定模塊的入口文件:main和exports。比較簡(jiǎn)單的模塊,可以只使用main字段,指定模塊加載的入口文件。// ./node_modules/es-module-package/package.json
{
"type": "module",
"main": "./src/index.js"
}
上面代碼指定項(xiàng)目的入口腳本為./src/index.js,它的格式為 ES6 模塊。如果沒(méi)有type字段,index.js就會(huì)被解釋為 CommonJS 模塊。然后,import命令就可以加載這個(gè)模塊。// ./my-app.mjs
import { something } from 'es-module-package';
// 實(shí)際加載的是 ./node_modules/es-module-package/src/index.js
上面代碼中,運(yùn)行該腳本以后,Node.js 就會(huì)到./node_modules目錄下面,尋找es-module-package模塊,然后根據(jù)該模塊package.json的main字段去執(zhí)行入口文件。這時(shí),如果用 CommonJS 模塊的require()命令去加載es-module-package模塊會(huì)報(bào)錯(cuò),因?yàn)?CommonJS 模塊不能處理export命令。package.json 的 exports 字段
exports字段的優(yōu)先級(jí)高于main字段。它有多種用法。package.json文件的exports字段可以指定腳本或子目錄的別名。// ./node_modules/es-module-package/package.json
{
"exports": {
"./submodule": "./src/submodule.js"
}
}
上面的代碼指定src/submodule.js別名為submodule,然后就可以從別名加載這個(gè)文件。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
如果沒(méi)有指定別名,就不能用“模塊+腳本名”這種形式加載腳本。// 報(bào)錯(cuò)
import submodule from 'es-module-package/private-module.js';
// 不報(bào)錯(cuò)
import submodule from './node_modules/es-module-package/private-module.js';
exports字段的別名如果是.,就代表模塊的主入口,優(yōu)先級(jí)高于main字段,并且可以直接簡(jiǎn)寫(xiě)成exports字段的值。{
"exports": {
".": "./main.js"
}
}
// 等同于
{
"exports": "./main.js"
}
由于exports字段只有支持 ES6 的 Node.js 才認(rèn)識(shí),所以可以用來(lái)兼容舊版本的 Node.js。{
"main": "./main-legacy.cjs",
"exports": {
".": "./main-modern.cjs"
}
}
上面代碼中,老版本的 Node.js (不支持 ES6 模塊)的入口文件是main-legacy.cjs,新版本的 Node.js 的入口文件是main-modern.cjs。利用.這個(gè)別名,可以為 ES6 模塊和 CommonJS 指定不同的入口。目前,這個(gè)功能需要在 Node.js 運(yùn)行的時(shí)候,打開(kāi)--experimental-conditional-exports標(biāo)志。{
"type": "module",
"exports": {
".": {
"require": "./main.cjs",
"default": "./main.js"
}
}
}
上面代碼中,別名.的require條件指定require()命令的入口文件(即 CommonJS 的入口),default條件指定其他情況的入口(即 ES6 的入口)。上面的寫(xiě)法可以簡(jiǎn)寫(xiě)如下。{
"exports": {
"require": "./main.cjs",
"default": "./main.js"
}
}
注意,如果同時(shí)還有其他別名,就不能采用簡(jiǎn)寫(xiě),否則或報(bào)錯(cuò)。{
// 報(bào)錯(cuò)
"exports": {
"./feature": "./lib/feature.js",
"require": "./main.cjs",
"default": "./main.js"
}
}
CommonJS 模塊加載 ES6 模塊
CommonJS 的require()命令不能加載 ES6 模塊,會(huì)報(bào)錯(cuò),只能使用import()這個(gè)方法加載。(async () => {
await import('./my-app.mjs');
})();
上面代碼可以在 CommonJS 模塊中運(yùn)行。require()不支持 ES6 模塊的一個(gè)原因是,它是同步加載,而 ES6 模塊內(nèi)部可以使用頂層await命令,導(dǎo)致無(wú)法被同步加載。ES6 模塊加載 CommonJS 模塊
ES6 模塊的import命令可以加載 CommonJS 模塊,但是只能整體加載,不能只加載單一的輸出項(xiàng)。// 正確
import packageMain from 'commonjs-package';
// 報(bào)錯(cuò)
import { method } from 'commonjs-package';
這是因?yàn)?ES6 模塊需要支持靜態(tài)代碼分析,而 CommonJS 模塊的輸出接口是module.exports,是一個(gè)對(duì)象,無(wú)法被靜態(tài)分析,所以只能整體加載。加載單一的輸出項(xiàng),可以寫(xiě)成下面這樣。import packageMain from 'commonjs-package';
const { method } = packageMain;
還有一種變通的加載方法,就是使用 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 模塊通過(guò)module.createRequire()方法可以加載 CommonJS 模塊。但是,這種寫(xiě)法等于將 ES6 和 CommonJS 混在一起了,所以不建議使用。同時(shí)支持兩種格式的模塊
一個(gè)模塊同時(shí)要支持 CommonJS 和 ES6 兩種格式,也很容易。如果原始模塊是 ES6 格式,那么需要給出一個(gè)整體輸出接口,比如export default obj,使得 CommonJS 可以用import()進(jìn)行加載。如果原始模塊是 CommonJS 格式,那么可以加一個(gè)包裝層。import cjsModule from '../index.js';
export const foo = cjsModule.foo;
上面代碼先整體輸入 CommonJS 模塊,然后再根據(jù)需要輸出具名接口。你可以把這個(gè)文件的后綴名改為.mjs,或者將它放在一個(gè)子目錄,再在這個(gè)子目錄里面放一個(gè)單獨(dú)的package.json文件,指明{ type: "module" }。另一種做法是在package.json文件的exports字段,指明兩種格式模塊各自的加載入口。"exports":{
"require": "./index.js",
"import": "./esm/wrapper.js"
}
上面代碼指定require()和import,加載該模塊會(huì)自動(dòng)切換到不一樣的入口文件。Node.js 的內(nèi)置模塊
Node.js 的內(nèi)置模塊可以整體加載,也可以加載指定的輸出項(xiàng)。// 整體加載
import EventEmitter from 'events';
const e = new EventEmitter();
// 加載指定的輸出項(xiàng)
import { readFile } from 'fs';
readFile('./foo.txt', (err, source) => {
if (err) {
console.error(err);
} else {
console.log(source);
}
});
加載路徑
ES6 模塊的加載路徑必須給出腳本的完整路徑,不能省略腳本的后綴名。import命令和package.json文件的main字段如果省略腳本的后綴名,會(huì)報(bào)錯(cuò)。// ES6 模塊中將報(bào)錯(cuò)
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 會(huì)按 URL 規(guī)則解讀。同一個(gè)腳本只要參數(shù)不同,就會(huì)被加載多次,并且保存成不同的緩存。由于這個(gè)原因,只要文件名中含有:、%、#、?等特殊字符,最好對(duì)這些字符進(jìn)行轉(zhuǎn)義。目前,Node.js 的import命令只支持加載本地模塊(file:協(xié)議)和data:協(xié)議,不支持加載遠(yuǎn)程模塊。另外,腳本路徑只支持相對(duì)路徑,不支持絕對(duì)路徑(即以/或//開(kāi)頭的路徑)。內(nèi)部變量
ES6 模塊應(yīng)該是通用的,同一個(gè)模塊不用修改,就可以用在瀏覽器環(huán)境和服務(wù)器環(huán)境。為了達(dá)到這個(gè)目標(biāo),Node.js 規(guī)定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內(nèi)部變量。首先,就是this關(guān)鍵字。ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當(dāng)前模塊,這是兩者的一個(gè)重大差異。其次,以下這些頂層變量在 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)致遞歸加載,使得程序無(wú)法執(zhí)行,因此應(yīng)該避免出現(xiàn)。但是實(shí)際上,這是很難避免的,尤其是依賴關(guān)系復(fù)雜的大項(xiàng)目,很容易出現(xiàn)a依賴b,b依賴c,c又依賴a這樣的情況。這意味著,模塊加載機(jī)制必須考慮“循環(huán)加載”的情況。對(duì)于 JavaScript 語(yǔ)言來(lái)說(shuō),目前最常見(jiàn)的兩種模塊格式 CommonJS 和 ES6,處理“循環(huán)加載”的方法是不一樣的,返回的結(jié)果也不一樣。CommonJS 模塊的加載原理
介紹 ES6 如何處理“循環(huán)加載”之前,先介紹目前最流行的 CommonJS 模塊格式的加載原理。CommonJS 的一個(gè)模塊,就是一個(gè)腳本文件。require命令第一次加載該腳本,就會(huì)執(zhí)行整個(gè)腳本,然后在內(nèi)存生成一個(gè)對(duì)象。{
id: '...',
exports: { ... },
loaded: true,
...
}
上面代碼就是 Node 內(nèi)部加載模塊后生成的一個(gè)對(duì)象。該對(duì)象的id屬性是模塊名,exports屬性是模塊輸出的各個(gè)接口,loaded屬性是一個(gè)布爾值,表示該模塊的腳本是否執(zhí)行完畢。其他還有很多屬性,這里都省略了。以后需要用到這個(gè)模塊的時(shí)候,就會(huì)到exports屬性上面取值。即使再次執(zhí)行require命令,也不會(huì)再次執(zhí)行該模塊,而是到緩存之中取值。也就是說(shuō),CommonJS 模塊無(wú)論加載多少次,都只會(huì)在第一次加載時(shí)運(yùn)行一次,以后再加載,就返回第一次運(yùn)行的結(jié)果,除非手動(dòng)清除系統(tǒng)緩存。CommonJS 模塊的循環(huán)加載
CommonJS 模塊的重要特性是加載時(shí)執(zhí)行,即腳本代碼在require的時(shí)候,就會(huì)全部執(zhí)行。一旦出現(xiàn)某個(gè)模塊被"循環(huán)加載",就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會(huì)輸出。讓我們來(lái)看,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腳本先輸出一個(gè)done變量,然后加載另一個(gè)腳本文件b.js。注意,此時(shí)a.js代碼就停在這里,等待b.js執(zhí)行完畢,再往下執(zhí)行。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í)行到第二行,就會(huì)去加載a.js,這時(shí),就發(fā)生了“循環(huán)加載”。系統(tǒng)會(huì)去a.js模塊對(duì)應(yīng)對(duì)象的exports屬性取值,可是因?yàn)閍.js還沒(méi)有執(zhí)行完,從exports屬性只能取回已經(jīng)執(zhí)行的部分,而不是最后的值。a.js已經(jīng)執(zhí)行的部分,只有一行。因此,對(duì)于b.js來(lái)說(shuō),它從a.js只輸入一個(gè)變量done,值為false。然后,b.js接著往下執(zhí)行,等到全部執(zhí)行完畢,再把執(zhí)行權(quán)交還給a.js。于是,a.js接著往下執(zhí)行,直到執(zhí)行完畢。我們寫(xiě)一個(gè)腳本main.js,驗(yàn)證這個(gè)過(guò)程。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,運(yùn)行結(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沒(méi)有執(zhí)行完畢,只執(zhí)行了第一行。二是,main.js執(zhí)行到第二行時(shí),不會(huì)再次執(zhí)行b.js,而是輸出緩存的b.js的執(zhí)行結(jié)果,即它的第四行。總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。另外,由于 CommonJS 模塊遇到循環(huán)加載時(shí),返回的是當(dāng)前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值,兩者可能會(huì)有差異。所以,輸入變量的時(shí)候,必須非常小心。var a = require('a'); // 安全的寫(xiě)法
var foo = require('a').foo; // 危險(xiǎn)的寫(xiě)法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一個(gè)部分加載時(shí)的值
};
上面代碼中,如果發(fā)生循環(huán)加載,require('a').foo的值很可能后面會(huì)被改寫(xiě),改用require('a')會(huì)更保險(xiǎn)一點(diǎn)。ES6 模塊的循環(huán)加載
ES6 處理“循環(huán)加載”與 CommonJS 有本質(zhì)的不同。ES6 模塊是動(dòng)態(tài)引用,如果使用import從一個(gè)模塊加載變量(即import foo from 'foo'),那些變量不會(huì)被緩存,而是成為一個(gè)指向被加載模塊的引用,需要開(kāi)發(fā)者自己保證,真正取值的時(shí)候能夠取到值。// 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以后會(huì)報(bào)錯(cuò),foo變量未定義,這是為什么?讓我們一行行來(lái)看,ES6 循環(huán)加載是怎么處理的。首先,執(zhí)行a.mjs以后,引擎發(fā)現(xiàn)它加載了b.mjs,因此會(huì)優(yōu)先執(zhí)行b.mjs,然后再執(zhí)行a.mjs。接著,執(zhí)行b.mjs的時(shí)候,已知它從a.mjs輸入了foo接口,這時(shí)不會(huì)去執(zhí)行a.mjs,而是認(rèn)為這個(gè)接口已經(jīng)存在了,繼續(xù)往下執(zhí)行。執(zhí)行到第三行console.log(foo)的時(shí)候,才發(fā)現(xiàn)這個(gè)接口根本沒(méi)定義,因此報(bào)錯(cuò)。解決這個(gè)問(wèn)題的方法,就是讓b.mjs運(yùn)行的時(shí)候,foo已經(jīng)有定義了。這可以通過(guò)將foo寫(xiě)成函數(shù)來(lái)解決。// 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};
這時(shí)再執(zhí)行a.mjs就可以得到預(yù)期結(jié)果。$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
這是因?yàn)楹瘮?shù)具有提升作用,在執(zhí)行import {bar} from './b'時(shí),函數(shù)foo就已經(jīng)有定義了,所以b.mjs加載的時(shí)候不會(huì)報(bào)錯(cuò)。這也意味著,如果把函數(shù)foo改寫(xiě)成函數(shù)表達(dá)式,也會(huì)報(bào)錯(cuò)。// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};
上面代碼的第四行,改成了函數(shù)表達(dá)式,就不具有提升作用,執(zhí)行就會(huì)報(bào)錯(cuò)。我們?cè)賮?lái)看 ES6 模塊加載器SystemJS給出的一個(gè)例子。// 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有一個(gè)參數(shù)n,只要不等于 0,就會(huì)減去 1,傳入加載的odd()。odd.js也會(huì)做類似操作。$ 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 的過(guò)程中,even()一共會(huì)執(zhí)行 6 次,所以變量counter等于 6。第二次調(diào)用even()時(shí),參數(shù)n從 20 變?yōu)?0,even()一共會(huì)執(zhí)行 11 次,加上前面的 6 次,所以變量counter等于 17。這個(gè)例子要是改寫(xiě)成 CommonJS,就根本無(wú)法執(zhí)行,會(huì)報(bào)錯(cuò)。// 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)加載”。這時(shí),執(zhí)行引擎就會(huì)輸出even.js已經(jīng)執(zhí)行的部分(不存在任何結(jié)果),所以在odd.js之中,變量even等于undefined,等到后面調(diào)用even(n - 1)就會(huì)報(bào)錯(cuò)。$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function