commonJs、AMD和ES6模塊化的總結

歷史上,JavaScript一直沒有模塊(module)體系,沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼裝起來。其餘語言都有這項功能,好比Ruby的require、Python的import,甚至就連css都有@import,可是javascript任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了巨大障礙。 爲了解決模塊化的問題,ES5中提供了AMD、CMD、CommonJs模塊化編程方案,ES6中新增了export/import。javascript

CommonJS規範

CommonJS就是一個JavaScript模塊化的規範,是用在服務器端的node的模塊規範,前端的webpack也是對CommonJS原生支持的。 在CommonJS規範中,每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。每一個模塊內部,module變量表明當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。 require方法用於加載模塊。 CommonJS的特色:css

  • 全部代碼都運行在模塊做用域,不會污染全局做用域。
  • 模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
  • 模塊加載的順序,按照其在代碼中出現的順序。 Node內部提供一個Module構建函數。全部模塊都是Module的實例。
function Module(id, parent) {

  this.id = id;

  this.exports = {};

  this.parent = parent;

  this.filename = null;

  this.loaded = false;

  this.children = [];

}

var module = new Module(filename, parent);

module.exports = Module;

複製代碼

module.exports屬性表示當前模塊對外輸出的接口,其餘文件加載該模塊,實際上就是讀取module.exports變量。 例如,咱們在moduleA.js文件中定義funA方法,並用module.exports變量把該方法暴露出,實例代碼以下:html

//moduleA.js

module.exports.funcA= function(){

  console.log('This is moduleA!');

}
複製代碼

而後,在moduleB模塊中加載引入moduleA模塊,即可以使用funA方法了,示例代碼以下:前端

//moduleB.js

var a = require('./moduleA');

a.funcA();//打印'This is moduleA!'
複製代碼

Node爲每一個模塊提供一個exports變量,指向module.exports。在對外輸出模塊接口時,能夠向exports對象添加方法,但不能對exports從新賦值。由於若是對exports從新賦值,會改版exports的指向,而導出的時候require的是module.exports不是exports,所以在exports上的操做就無效了。 模塊引用經過require實現,主要有如下三種方式java

var httpModule=require('HTTP');//用 「模塊名」加載服務模塊http

var b=require('./user/b');//用「相對路徑」加載文件b.js

var b=require('../ home/user/c');//用「絕對路徑」加載文件c.js
複製代碼

根據參數的不一樣格式,require命令去不一樣路徑尋找模塊文件。加載規則以下:node

(1)若是參數字符串以「/」開頭,則表示加載的是一個位於絕對路徑的模塊文件。好比,require('/home/marco/foo.js')將加載/home/marco/foo.js。webpack

(2)若是參數字符串以「./」開頭,則表示加載的是一個位於相對路徑(跟當前執行腳本的位置相比)的模塊文件。好比,require('./circle')將加載當前腳本同一目錄的circle.js。web

(3)若是參數字符串不以「./「或」/「開頭,則表示加載的是一個默認提供的核心模塊(位於Node的系統安裝目錄中),或者一個位於各級node_modules目錄的已安裝模塊(全局安裝或局部安裝)。面試

(4)若是參數字符串不以「./「或」/「開頭,並且是一個路徑,好比require('example-module/path/to/file'),則將先找到example-module的位置,而後再以它爲參數,找到後續路徑。編程

(5)若是指定的模塊文件沒有發現,Node會嘗試爲文件名添加.js、.json、.node後,再去搜索。.js件會以文本格式的JavaScript腳本文件解析,.json文件會以JSON格式的文本文件解析,.node文件會以編譯後的二進制文件解析。

(6)若是想獲得require命令加載的確切文件名,使用require.resolve()方法。 CommonJS是同步的,意味着你想調用模塊裏的方法,必須先用require加載模塊。這對服務器端的Nodejs來講不是問題,由於模塊的JS文件都在本地硬盤上,CPU的讀取時間很是快,同步不是問題。但若是是瀏覽器環境,要從服務器加載模塊。模塊的加載將取決於網速,若是採用同步,網絡情緒不穩定時,頁面可能卡住,這就必須採用異步模式。因此,就有了 AMD解決方案。

AMD

AMD是Asynchronous Module Definition,即‘異步模塊定義’,它是一個在瀏覽器前端實現模塊化的規範,RequireJS是對這個規範的實現。 RequireJS主要解決兩個問題

  • 無多個js文件可能有依賴關係,被依賴的文件須要早於依賴它的文件加載到瀏覽器
  • js加載的時候瀏覽器會中止頁面渲染,加載文件越多,頁面失去響應時間越長

AMD有兩個API,define用於定義模塊,require用於調用模塊。

define

RequireJS要求每一個模塊放在一個單獨的文件裏,按照是否依賴其餘模塊,能夠分紅兩種狀況討論: 1.定義獨立模塊

define(function(){
  return {
    method1:function(){},
    method2:function(){}
  }
})
複製代碼

2.定義非獨立模塊,所定義的模塊須要依賴其餘模塊

//AMD實例alpha,依賴require,exports,beta
define('alpha',["require", "exports", "beta"],function(require,exports,beta){
  exports.verb = function() {
 
         return beta.verb();
 
      //或者:
 
      return require("beta").verb();
})

//匿名模塊
define(["alpha"],function (alpha){
  return {
      verb: function(){
          return alpha.verb() + 2;
      }
  }; 
})
複製代碼

require

require(
    [ "backbone" ], 
    function ( Backbone ) {
        return Backbone.View.extend({ /* ... */ });
    }, 
    function (err) { //錯誤處理函數
        // ...
    }
);

複製代碼

實際應用

//定義M模塊,本申明一個全局變量
define('M',[],function(){
    window.M={};
    return M;
})
//定義模塊a 依賴模塊 M,b,c
define('a',['M','b','c'],function(M){
    alert(M.ob);
    alert(M.oc);
})
//定義b模塊
define('b',[],function(){
    M.ob = 2;
    return M;
})
//定義c模塊
define('c',[],function(){
    M.oc = 3;
    return M;
})
//引入a模塊
require(['a'],function(a){
    
})

複製代碼

CMD

CMD 即Common Module Definition通用模塊定義,CMD規範是國內發展出來的,就像AMD有個requireJS,CMD有個瀏覽器的實現SeaJS,SeaJS要解決的問題和requireJS同樣,只不過在模塊定義方式和模塊加載(能夠說運行、解析)時機上有所不一樣。Sea.js 推崇一個模塊一個文件,遵循統一的寫法。 CMD格式以下

define(function(require, exports, module) {

  // 模塊代碼

});
複製代碼

CMD和AMD的區別

  • AMD是依賴關係前置,在定義模塊的時候就要聲明其依賴的模塊;
  • CMD是按需加載依賴就近,只有在用到某個模塊的時候再去require;
// CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // 此處略去 100 行
  var b = require('./b') // 依賴能夠就近書寫
  b.doSomething()
  // ... 
})

// AMD 默認推薦的是
define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好
  a.doSomething()
  // 此處略去 100 行
  b.doSomething()
  ...
}) 
複製代碼

ES6模塊化import

ES6模塊的設計思想,是儘可能的靜態化,使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。CommonJS和AMD模塊,都只能在運行時肯定這些東西。好比,CommonJS模塊就是對象,輸入時必須查找對象屬性。

//CommonJS模塊
let {stat,exists,readFile} = require('fs')

//等同於
let _fs = require('fs');
let stat = _fs.stsat, exists = _fs.exists, readFile = _fs.readFile;
複製代碼

上面代碼的實質是總體加載fs模塊(即加載fs的全部方法),生成一個對象( ),而後再從這個對象上面讀取3個方法。這種加載稱爲"運行時加載" ,由於只有運行時才能獲得這個對象,致使徹底沒辦法在編譯時作靜態優化。 ES6模塊不是對象,而是經過export命令顯式指定輸出的代碼,輸入時也採用靜態命令的形式。

//ES6模塊
import {stat, exists, readFile} from 'fs';
複製代碼

上面代碼的實質是從fs模塊加載3個方法,其餘方法不加載。這種加載稱爲編譯時加載 ,即ES6能夠在編譯時就完成模塊加載,效率要比CommonJS模塊的加載方式高。固然,這也致使了無法引用ES6模塊自己,由於它不是對象。 在 ES6 模塊中,不管你是否加入「use strict;」語句,默認狀況下模塊都是在嚴格模式下運行。

default export 默認導出

一個模塊只能有一個默認導出,對於默認導出,導入的名稱能夠和導出的名稱不一致。

/******************************導出**********************/
export default function(){
    return "默認導出一個方法"
}
/******************************引入**********************/
import myFn from "./test.js";//注意這裏默認導出不須要用{}。
console.log(myFn());//默認導出一個方法
複製代碼

Node的默認模塊格式是CommonJS,要經過Babel這樣的轉碼器,在Node裏面使用ES6模塊

js模塊化總結

名稱 CommonJS AMD CMD ES6
API module.exports+require define+require define+require export+import
執行環境 服務端 客戶端 客戶端 服務端+客戶端
執行方式 運行時加載 運行時加載 運行時加載 編譯時加載
同步/異步 同步 異步 須要時加載

循環加載

在大型項目中,常常會有a依賴b,b依賴c,c又依賴a的狀況,模塊加載機制必須考慮"循環加載"的狀況。CommonJS和ES6的循環加載機制是不一樣的。

CommonJS的循環加載

CommonJS的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。 CommonJS的作法是,一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。 舉例

//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);
複製代碼

上面的代碼依賴了a.js和b.js兩個文件,node main.js,執行到第二行的時候,會去執行a.js文件

//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執行到第二行,會在這個地方停住,去加載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,因此a.done = false。 而後,b.js接着往下執行,等到所有執行完畢,再把執行權交還給a.js。因而,a.js接着往下執行,直到執行完畢。 main.js的執行結果是

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

ES6模塊的循環加載

ES6模塊的運行機制與CommonJS不同,它遇到模塊加載命令import時,不會去執行模塊,而是隻生成一個引用。等到真的須要用到時,再到模塊裏面去取值。 所以,ES6模塊是動態引用,不存在緩存值的問題,並且模塊裏面的變量,綁定其所在的模塊。請看下面的例子。

// 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處理"循環加載"與CommonJS有本質的不一樣。ES6根本不會關心是否發生了"循環加載",只是生成一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。 請看下面的例子

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

// b.js
import {foo} from './a.js';
export function bar() {  
  if (Math.random() > 0.5) {
    foo();
  }
}
複製代碼

按照CommonJS規範,上面的代碼是無法執行的。a先加載b,而後b又加載a,這時a尚未任何執行結果,因此輸出結果爲null,即對於b.js來講,變量foo的值等於null,後面的foo()就會報錯。

可是,ES6能夠執行上面的代碼。

$ babel-node a.js

執行完畢
複製代碼

再來看一個例子

// 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裏面的函數foo有一個參數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的過程當中,foo()一共會執行6次,因此變量counter等於6。第二次調用even()時,參數n從20變爲0,foo()一共會執行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 複製代碼

模塊化相關面試題

1.a.js 和 b.js 兩個文件互相 require 是否會死循環? 雙方是否能導出變量? 如何從設計上避免這種問題? 答:這個問題考察的是JavaScript 模塊的循環加載不會死循環,雙方按照順序同步執行,導出的是已經執行部分的副本。

2.若是 a.js require 了 b.js, 那麼在 b 中定義全局變量 t = 111 可否在 a 中直接打印出來? 答:每一個 .js 能獨立一個環境只是由於 node 幫你在外層包了一圈自執行, 因此你使用 t = 111 定義全局變量在其餘地方固然能拿到. 狀況以下:

// b.js
(function (exports, require, module, __filename, __dirname) {
  t = 111;
})();

// a.js
(function (exports, require, module, __filename, __dirname) {
  // ...
  console.log(t); // 111
})();
複製代碼

附加題

require源碼

相關文章
相關標籤/搜索