它們有兩個重大差異:
- CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
- 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 不一樣。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊里面去取值。換句話說,ES6 的import有點像 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變量。
第二個差異:
因為 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。
Es6模塊的設計思想是盡量放入靜態化,使得在編譯時就能確定依賴關系,而CommonJS就只能在運行時確定這些輸入和輸出的變量。
// CommonJS模塊 let { stat, exists, readFile } = require('fs'); // 等同於 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;
上面代碼的實質是整體加載fs模塊(即加載fs的所有方法),生成一個對象(_fs),然后再從這個對象上面讀取 3 個方法。這種加載稱為“運行時加載”,因為只有運行時才能得到這個對象,導致完全沒辦法在編譯時做“靜態優化”。
ES6 通過export命令顯式指定輸出的代碼,再通過import命令輸入。
// ES6模塊 import { stat, exists, readFile } from 'fs';
上面代碼的實質是從fs模塊加載 3 個方法,其他方法不加載。這種加載稱為“編譯時加載”或者靜態加載,即 ES6 可以在編譯時就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。
在es6中,export語句輸出的接口,與其對應的值是動態綁定關系,即通過該接口,可以取到模塊內部實時的值。
export var foo = 'bar'; setTimeout(() => foo = 'baz', 500);
上面代碼輸出變量foo,值為bar,500 毫秒之后變成baz。
這一點與 CommonJS 規范完全不同。CommonJS 模塊輸出的是值的緩存,不存在動態更新。
export命令可以出現在模塊的任何位置,只要處於模塊頂層就可以。如果處於塊級作用域內,就會報錯,import命令也是如此。這是因為處於條件代碼塊之中,就沒法做靜態優化了,違背了 ES6 模塊的設計初衷。
function foo() { export default 'bar' // SyntaxError } foo()
上面代碼中,export語句放在函數之中,結果報錯。
注意,import命令具有提升效果,會提升到整個模塊的頭部,首先執行。
foo();
import { foo } from 'my_module';
上面的代碼不會報錯,因為import的執行早於foo的調用。這種行為的本質是,import命令是編譯階段執行的,在代碼運行之前。
由於import是靜態執行,所以不能使用表達式和變量,這些只有在運行時才能得到結果的語法結構。
// 報錯 import { 'f' + 'oo' } from 'my_module'; // 報錯 let module = 'my_module'; import { foo } from module; // 報錯 if (x === 1) { import { foo } from 'module1'; } else { import { foo } from 'module2'; }
上面三種寫法都會報錯,因為它們用到了表達式、變量和if結構。在靜態分析階段,這些語法都是沒法得到值的。
這樣的設計,固然有利於編譯器提高效率,但也導致無法在運行時加載模塊。在語法上,條件加載就不可能實現。如果import命令要取代 Node 的require方法,這就形成了一個障礙。因為require是運行時加載模塊,import命令無法取代require的動態加載功能。
const path = './' + fileName;
const myModual = require(path);
上面的語句就是動態加載,require到底加載哪一個模塊,只有運行時才知道。import命令做不到這一點。
因此,有一個提案,建議引入import()函數,完成動態加載。
import(specifier)
上面代碼中,import函數的參數specifier,指定所要加載的模塊的位置。import命令能夠接受什么參數,import()函數就能接受什么參數,兩者區別主要是后者為動態加載。
import()返回一個 Promise 對象。下面是一個例子。
const main = document.querySelector('main'); import(`./section-modules/${someVariable}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContent = err.message; });
import()函數可以用在任何地方,不僅僅是模塊,非模塊的腳本也可以使用。它是運行時執行,也就是說,什么時候運行到這一句,就會加載指定的模塊。另外,import()函數與所加載的模塊沒有靜態連接關系,這點也是與import語句不相同。import()類似於 Node 的require方法,區別主要是前者是異步加載,后者是同步加載。
目前階段,通過 Babel 轉碼,CommonJS 模塊的require命令和 ES6 模塊的import命令,可以寫在同一個模塊里面,但是最好不要這樣做。因為import在靜態解析階段執行,所以它是一個模塊之中最早執行的。下面的代碼可能不會得到預期結果。
require('core-js/modules/es6.symbol'); require('core-js/modules/es6.promise'); import React from 'React';
注:
import命令輸入的變量都是只讀的,因為它的本質是輸入接口。也就是說,不允許在加載模塊的腳本里面,改寫接口。
import {a} from './xxx.js' a = {}; // Syntax Error : 'a' is read-only;
上面代碼中,腳本加載了變量a,對其重新賦值就會報錯,因為a是一個只讀的接口。但是,如果a是一個對象,改寫a的屬性是允許的。
import {a} from './xxx.js' a.foo = 'hello'; // 合法操作
上面代碼中,a的屬性可以成功改寫,並且其他模塊也可以讀到改寫后的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都當作完全只讀,輕易不要改變它的屬性。
參考:https://blog.csdn.net/jackTesla/java/article/details/80796936