1. 程式人生 > 實用技巧 >es6 模組與commonJS的區別

es6 模組與commonJS的區別

在剛接觸模組化開發的階段,我總是容易將export、import、require等語法給弄混,今天索性記個筆記,將ES6 模組
知識點理清楚

未接觸ES6 模組時,模組開發方案常見的有CommonJS、AMD、CMD三種。CommonJS用於伺服器,而另外兩種是用於瀏覽器。
接觸ES6 模組後,模組體系變得更加完善,功能實現更簡單,伺服器和瀏覽器都通用,完全可以取代常見的三種規範。今天就記一下es6模組和CommonJS的區別。

1、載入方式

2、值的運用

3、迴圈載入

一、載入方式

ES6 模組的設計思想,是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。CommonJS 和 AMD、CMD 模組,都只能在執行時確定這些東西。

比如,CommonJS 模組就是物件,輸入時必須查詢物件屬性。

// CommonJS模組
let { stat, exists, readFile } = require('fs'); // 等同於
let _fs = require('fs');
let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;

  

上面的程式碼實質是整體載入了模組fs的所有方法,生成了一個_fs物件,然後從這個物件讀取三個方法。如果模組裡沒有很多方法的話,這樣開發貌似是不錯的,
可是一旦方法多了起來,那效能將大大降低,載入了多餘的方法,浪費我的寬頻。這載入簡稱點就是“執行後載入”。

而ES6 模組不是物件,而是通過export命令顯式指定輸出的程式碼,再通過import命令輸入。

// ES6模組
import { stat, exists, readFile } from 'fs';

上面程式碼的實質是從fs模組載入3個方法,其他方法不載入。這種載入稱為“編譯時載入”或者靜態載入,即 ES6 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。當然,這也導致了沒法引用 ES6 模組本身,因為它不是物件。

由於 ES6 模組是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態載入帶來的各種好處,ES6 模組還有以下好處。

  • 不再需要UMD模組格式了,將來伺服器和瀏覽器都會支援 ES6 模組格式。目前,通過各種工具庫,其實已經做到了這一點。
  • 將來瀏覽器的新 API 就能用模組格式提供,不再必要做成全域性變數或者navigator物件的屬性。
  • 不再需要物件作為名稱空間(比如Math物件),未來這些功能可以通過模組提供。

二、值的引用

除了載入方式的不一樣,在模組值的運用也有不一樣的特點。CommonJS模組輸出的是一個值的拷貝,而ES6模組輸出的是值的引用。

CommonJS模組輸出的是被輸出值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。請看下面這個模組檔案lib.js的例子。

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

上面程式碼輸出內部變數counter和改寫這個變數的內部方法incCounter。然後,在main.js裡面載入這個模組。  

// main.js
var mod = require('./lib'); console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3

上面程式碼說明,lib.js模組載入以後,它的內部變化就影響不到輸出的mod.counter了。這是因為mod.counter是一個原始型別的值,會被快取。除非寫成一個函式,才能得到內部變動後的值。

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

上面程式碼中,輸出的counter屬性實際上是一個取值器函式。現在再執行main.js,就可以正確讀取內部變數counter的變動了。  

ES6模組的執行機制與CommonJS不一樣,它遇到模組載入命令import時,不會去執行模組,而是隻生成一個動態的只讀引用。等到真的需要用到時,再到模組裡面去取值,換句話說,ES6的輸入有點像Unix系統的“符號連線”,原始值變了,import輸入的值也會跟著變。因此,ES6模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。

還是舉上面的例子。  

// 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是活的,完全反應其所在模組lib.js內部的變化。

再舉一個出現在export一節中的例子。

// 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毫秒,又變為等於baz。

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

$ babel-node m2.js
bar
baz

上面程式碼表明,ES6模組不會快取執行結果,而是動態地去被載入的模組取值,並且變數總是繫結其所在的模組。

由於ES6輸入的模組變數,只是一個“符號連線”,所以這個變數是隻讀的,對它進行重新賦值會報錯。

// 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創造了一個名為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';

現在執行main.js,輸出的是1。

$ babel-node main.js
1

這就證明瞭x.js和y.js載入的都是C的同一個例項。  

三、迴圈載入

“迴圈載入”(circular dependency)指的是,a指令碼的執行依賴b指令碼,而b指令碼的執行又依賴a指令碼。  

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

通常,“迴圈載入”表示存在強耦合,如果處理不好,還可能導致遞迴載入,使得程式無法執行,因此應該避免出現。

但是實際上,這是很難避免的,尤其是依賴關係複雜的大專案,很容易出現a依賴bb依賴cc又依賴a這樣的情況。這意味著,模組載入機制必須考慮“迴圈載入”的情況。

對於JavaScript語言來說,目前最常見的兩種模組格式CommonJS和ES6,處理“迴圈載入”的方法是不一樣的,返回的結果也不一樣。

CommonJS模組的迴圈載入

CommonJS模組的重要特性是載入時執行,即指令碼程式碼在require的時候,就會全部執行。一旦出現某個模組被"迴圈載入",就只輸出已經執行的部分,還未執行的部分不會輸出。

讓我們來看,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 執行完畢');

上面程式碼之中,a.js指令碼先輸出一個done變數,然後載入另一個指令碼檔案b.js。注意,此時a.js程式碼就停在這裡,等待b.js執行完畢,再往下執行。

再看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 執行完畢');

上面程式碼之中,b.js執行到第二行,就會去載入a.js,這時,就發生了“迴圈載入”。系統會去a.js模組對應物件的exports屬性取值,可是因為a.js還沒有執行完,從exports屬性只能取回已經執行的部分,而不是最後的值。

a.js已經執行的部分,只有一行。

exports.done = false;

  

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

然後,b.js接著往下執行,等到全部執行完畢,再把執行權交還給a.js。於是,a.js接著往下執行,直到執行完畢。我們寫一個指令碼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);

  

執行main.js,執行結果如下。

$ node main.js

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

  

上面的程式碼證明瞭兩件事。一是,在b.js之中,a.js沒有執行完畢,只執行了第一行。二是,main.js執行到第二行時,不會再次執行b.js,而是輸出快取的b.js的執行結果,即它的第四行。

exports.done = true;

  

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

另外,由於CommonJS模組遇到迴圈載入時,返回的是當前已經執行的部分的值,而不是程式碼全部執行後的值,兩者可能會有差異。所以,輸入變數的時候,必須非常小心。

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); // 使用的是一個部分載入時的值
};

  

上面程式碼中,如果發生迴圈載入,require('a').foo的值很可能後面會被改寫,改用require('a')會更保險一點。

ES6模組的迴圈載入

ES6處理“迴圈載入”與CommonJS有本質的不同。ES6模組是動態引用,如果使用import從一個模組載入變數(即import foo from 'foo'),那些變數不會被快取,而是成為一個指向被載入模組的引用,需要開發者自己保證,真正取值的時候能夠取到值。

請看下面這個例子。

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

  

上面程式碼中,a.js載入b.jsb.js又載入a.js,構成迴圈載入。執行a.js,結果如下。

$ babel-node a.js
b.js
undefined
a.js
bar

  

上面程式碼中,由於a.js的第一行是載入b.js,所以先執行的是b.js。而b.js的第一行又是載入a.js,這時由於a.js已經開始執行了,所以不會重複執行,而是繼續往下執行b.js,所以第一行輸出的是b.js

接著,b.js要列印變數foo,這時a.js還沒執行完,取不到foo的值,導致打印出來是undefinedb.js執行完,開始執行a.js,這時就一切正常了。

再看一個稍微複雜的例子(摘自 Dr. Axel Rauschmayer 的《Exploring ES6》)。

// a.js
import {bar} from './b.js';
export function foo() {
console.log('foo');
bar();
console.log('執行完畢');
}
foo(); // b.js
import {foo} from './a.js';
export function bar() {
console.log('bar');
if (Math.random() > 0.5) {
foo();
}
}

  

按照CommonJS規範,上面的程式碼是沒法執行的。a先載入b,然後b又載入a,這時a還沒有任何執行結果,所以輸出結果為null,即對於b.js來說,變數foo的值等於null,後面的foo()就會報錯。

但是,ES6可以執行上面的程式碼。

$ babel-node a.js
foo
bar
執行完畢 // 執行結果也有可能是
foo
bar
foo
bar
執行完畢
執行完畢

  

上面程式碼中,a.js之所以能夠執行,原因就在於ES6載入的變數,都是動態引用其所在的模組。只要引用存在,程式碼就能執行。

下面,我們詳細分析這段程式碼的執行過程。

// a.js

// 這一行建立一個引用,
// 從`b.js`引用`bar`
import {bar} from './b.js'; export function foo() {
// 執行時第一行輸出 foo
console.log('foo');
// 到 b.js 執行 bar
bar();
console.log('執行完畢');
}
foo(); // b.js // 建立`a.js`的`foo`引用
import {foo} from './a.js'; export function bar() {
// 執行時,第二行輸出 bar
console.log('bar');
// 遞迴執行 foo,一旦隨機數
// 小於等於0.5,就停止執行
if (Math.random() > 0.5) {
foo();
}
}

  

我們再來看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裡面的函式even有一個引數n,只要不等於0,就會減去1,傳入載入的odd()odd.js也會做類似操作。

執行上面這段程式碼,結果如下。

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

  

上面程式碼中,引數n從10變為0的過程中,even()一共會執行6次,所以變數counter等於6。第二次呼叫even()時,引數n從20變為0,even()一共會執行11次,加上前面的6次,所以變數counter等於17。

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

// 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,形成“迴圈載入”。這時,執行引擎就會輸出even.js已經執行的部分(不存在任何結果),所以在odd.js之中,變數even等於null,等到後面呼叫even(n-1)就會報錯。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function