這個問題也能夠變爲 commonjs模塊和ES6模塊的區別;下面就經過一些例子來講明它們的區別。前端
先來一道面試題測驗一下:下面代碼輸出什麼node
// base.js let count = 0; setTimeout(() => { console.log("base.count", ++count); // 1 }, 500) module.exports.count = count; // commonjs.js const { count } = require('./base'); setTimeout(() => { console.log("count is" + count + 'in commonjs'); // 0 }, 1000) // base1.js let count = 0; setTimeout(() => { console.log("base.count", ++count); // 1 }, 500) exports const count = count; // es6.js import { count } from './base1'; setTimeout(() => { console.log("count is" + count + 'in es6'); // 1 }, 1000)
注意上面的ES6模塊的代碼不能直接在 node 中執行。能夠把文件名稱後綴改成.mjs
, 而後執行node --experimental-modules es6.mjs
,或者自行配置babel。
CommonJs 規範規定,每一個模塊內部,module
變量表明當前模塊。這個變量是一個對象,它的 exports
屬性(即module.exports
)是對外的接口,加載某個模塊,實際上是加載該模塊的module.exports
屬性。es6
const x = 5; const addX = function (value) { return value + x; }; module.exports.x = x; module.exports.addX = addX;
上面代碼經過module.exports輸出變量x和函數addX。面試
require方法用於加載模塊。數組
const example = require('./example.js'); console.log(example.x); // 5 console.log(example.addX(1)); // 6
CommonJS 模塊的特色以下:緩存
Node內部提供一個Module構建函數。全部模塊都是Module的實例。微信
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; // ... }
每一個模塊內部,都有一個module對象,表明當前模塊。它有如下屬性。babel
module.exports屬性表示當前模塊對外輸出的接口,其餘文件加載該模塊,實際上就是讀取module.exports變量。app
爲了方便,Node爲每一個模塊提供一個exports變量,指向module.exports。這等同在每一個模塊頭部,有一行這樣的命令函數
const exports = module.exports;
注意,不能直接將exports變量指向一個值,由於這樣等於切斷了exports與module.exports的聯繫。
exports = function(x) {console.log(x)};
上面這樣的寫法是無效的,由於exports再也不指向module.exports了。
下面的寫法也是無效的。
exports.hello = function() { return 'hello'; }; module.exports = 'Hello world';
上面代碼中,hello函數是沒法對外輸出的,由於module.exports被從新賦值了。
這意味着,若是一個模塊的對外接口,就是一個單一的值,最好不要使用exports輸出,最好使用module.exports輸出。
module.exports = function (x){ console.log(x);};
若是你以爲,exports與module.exports之間的區別很難分清,一個簡單的處理方法,就是放棄使用exports,只使用module.exports。
第一次加載某個模塊時,Node會緩存該模塊。之後再加載該模塊,就直接從緩存取出該模塊的module.exports屬性。
require('./example.js'); require('./example.js').message = "hello"; require('./example.js').message // "hello"
上面代碼中,連續三次使用require命令,加載同一個模塊。第二次加載的時候,爲輸出的對象添加了一個message屬性。可是第三次加載的時候,這個message屬性依然存在,這就證實require命令並無從新加載模塊文件,而是輸出了緩存。
若是想要屢次執行某個模塊,可讓該模塊輸出一個函數,而後每次require這個模塊的時候,從新執行一下輸出的函數。
全部緩存的模塊保存在require.cache之中,若是想刪除模塊的緩存,能夠像下面這樣寫。
// 刪除指定模塊的緩存 delete require.cache[moduleName]; // 刪除全部模塊的緩存 Object.keys(require.cache).forEach(function(key) { delete require.cache[key]; })
注意,緩存是根據絕對路徑識別模塊的,若是一樣的模塊名,可是保存在不一樣的路徑,require命令仍是會從新加載該模塊。
ES6 模塊的設計思想是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時肯定這些東西。好比,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 模塊自己,由於它不是對象。
ES6的模塊功能主要由兩個命令構成:export
和 import
。 export 命令用於規定模塊的對外接口。import 命令用於輸入 其餘模塊提供的功能。
export const firstName = 'Michael'; export function multiply(x, y) { return x * y; };
// 報錯 export 1; // 報錯 const m = 1; export m;
上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出 1,第二種寫法經過變量m,仍是直接輸出 1。1只是一個值,不是接口。
// 寫法一 export const m = 1; // 寫法二 const m = 1; export {m}; // 寫法三 const n = 1; export {n as m};
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的屬性能夠成功改寫,而且其餘模塊也能夠讀到改寫後的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都看成徹底只讀,不要輕易改變它的屬性。
foo(); import { foo } from 'my_module';
這種行爲的本質是,import命令是編譯階段執行的,在代碼運行以前。
// 報錯 import { 'f' + 'oo' } from 'my_module'; // 報錯 let module = 'my_module'; import { foo } from module;
import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同於 import { foo, bar } from 'my_module';
上面代碼中,雖然foo和bar在兩個語句中加載,可是它們對應的是同一個my_module實例。也就是說,import語句是 Singleton
模式。
export default
就是輸出一個叫作default的變量或方法export default
因此它後面不能跟變量聲明語句export default
就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。// modules.js function sayHello() { console.log('哈哈哈') } export { sayHello as default}; // 等同於 // export default sayHello; // app.js import { default as sayHello } from 'modules'; // 等同於 // import sayHello from 'modules';
// 正確 export const a = 1; // 正確 const a = 1; export default a; // 錯誤 export default const a = 1;
上面代碼中,export default a的含義是將變量a的值賦給變量default。因此,最後一種寫法會報錯。
一樣地,由於export default命令的本質是將後面的值,賦給default變量,因此能夠直接將一個值寫在export default以後。
// 正確 export default 42; // 報錯 export 42;
上面代碼中,後一句報錯是由於沒有指定對外的接口,而前一句指定對外接口爲default。
export { foo, bar } from 'my_module'; // 能夠簡單理解爲 import { foo, bar } from 'my_module'; export { foo, bar };
寫成一行之後,foo和bar實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foo和bar。
export { es6 as default } from './someModule'; // 等同於 import { es6 } from './someModule'; export default es6;
在日常開發中這種常被用到,有一個utils目錄,目錄下面每一個文件都是一個工具函數,這時候常常會建立一個index.js文件做爲 utils的入口文件,index.js中引入utils目錄下的其餘文件,其實這個index.js其的做用就是一個對外轉發 utils 目錄下 全部工具函數的做用,這樣其餘在使用 utils 目錄下文件的時候能夠直接 經過 import { xxx } from './utils'
來引入。
第二個差別是由於 CommonJS 加載的是一個對象(即module.exports屬性)。該對象只有在腳本運行完纔會生成。而ES6模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態編譯階段就會生成。
在傳統編譯語言的流程中,程序中的一段源代碼在執行以前會經歷三個步驟,統稱爲編譯。」分詞/詞法分析「 -> 」解析/語法分析「 -> "代碼生成"。
下面來解釋一下第一個區別
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js的例子。
// lib.js const counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, };
上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。而後,在main.js裏面加載這個模塊。
// main.js const mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3
上面代碼說明,lib.js 模塊加載之後,它的內部變化就影響不到輸出的 mod.counter了。這是由於 mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值
// lib.js const counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, };
上面代碼中,輸出的counter屬性其實是一個取值器函數。如今再執行main.js,就能夠正確讀取內部變量counter的變更了。
3 4
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 const 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可否正確讀取這個變化。
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。
這就證實了x.js和y.js加載的都是C的同一個實例。
在日常開發中這種常被用到,有一個utils目錄,目錄下面每一個文件都是一個工具函數,這時候常常會建立一個index.js文件做爲 utils的入口文件,index.js中引入utils目錄下的其餘文件,其實這個index.js其的做用就是一個對外轉發 utils 目錄下 全部工具函數的做用,這樣其餘在使用 utils 目錄下文件的時候能夠直接 經過 import { xxx } from './utils' 來引入。
下面代碼輸出什麼
// index.js console.log('running index.js'); import { sum } from './sum.js'; console.log(sum(1, 2)); // sum.js console.log('running sum.js'); export const sum = (a, b) => a + b;
答案: running sum.js, running index.js, 3
。
import命令是編譯階段執行的,在代碼運行以前。所以這意味着被導入的模塊會先運行,而導入模塊的文件會後執行。
這是CommonJS中require()和import之間的區別。使用require(),您能夠在運行代碼時根據須要加載依賴項。 若是咱們使用require而不是import,running index.js,running sum.js,3會被依次打印。
// module.js export default () => "Hello world" export const name = "Lydia" // index.js import * as data from "./module" console.log(data)
答案:{ default: function default(), name: "Lydia" }
使用import * as name語法,咱們將module.js文件中全部export導入到index.js文件中,而且建立了一個名爲data的新對象。 在module.js文件中,有兩個導出:默認導出和命名導出。 默認導出是一個返回字符串「Hello World」的函數,命名導出是一個名爲name的變量,其值爲字符串「Lydia」。
data對象具備默認導出的default屬性,其餘屬性具備指定exports的名稱及其對應的值。
// counter.js let counter = 10; export default counter; // index.js import myCounter from "./counter"; myCounter += 1; console.log(myCounter);
答案:Error
引入的模塊是 只讀 的: 你不能修改引入的模塊。只有導出他們的模塊才能修改其值。
當咱們給myCounter增長一個值的時候會拋出一個異常: myCounter是隻讀的,不能被修改。
最近發起了一個100天前端進階計劃,主要是深挖每一個知識點背後的原理,歡迎關注 微信公衆號「牧碼的星星」,咱們一塊兒學習,打卡100天。