CommonJS、AMD、CMD、ES6 模塊規範講解

目標

經過閱讀本篇文章你能夠學習到:javascript

  • 原始模擬模塊的一些寫法
  • CommonJS規範
  • AMD規範
  • CMD規範
  • AMD和CMD的區別
  • ES6 Modules規範
  • CommonJS與ES6 Modules規範的區別

模塊化原始寫法

在沒有CommonJSES6的時候,咱們想要達到模塊化的效果可能有這麼三種:html

1. 一個函數就是一個模塊

<script>  
  function m1 () {  
    // ...  
  }  
  function m2 () {  
    // ...  
  }  
</script>
缺點:污染了全局變量,沒法保證不會與其它模塊發生衝突,並且模塊成員之間看不出直接關係。

2. 一個對象就是一個模塊

對象寫法 爲了解決上面的缺點,能夠把模塊寫成一個對象,全部的模塊成員都放到這個對象裏面。前端

index.htmljava

<script>  
  var module1 = new Object({  
    _sum: 0,  
    foo1: function () {},  
    foo2: function () {}  
  })  
</script>
缺點:會暴露全部模塊成員,內部的狀態可能被改寫。

例如,咱們若是隻是想暴露出兩個方法而不暴露出 _sum,就作不到。node

而此時,_sum 可能被外部改寫:jquery

module1._sum = 2;

3. 當即執行函數爲一個模塊

<script>  
  var module1 = (function() {  
    var _sum = 0;  
    var foo1 = function () {};  
    var foo2 = function () {};  
    return {  
      foo1: foo1,  
      foo2: foo2  
    }  
  })();  
</script>

利用當即執行函數內的做用域已經閉包來實現模塊功能,導出咱們想要導出的成員。面試

此時外部代碼就不能讀取到 _sum 了:npm

console.log(module1._sum) // undefined

CommonJS規範

這裏不作具體的介紹了,我只把一些重要的知識點以及混淆點例舉出來。json

主要是從這四個方面說:segmentfault

  • 暴露模塊
  • 引用模塊
  • 模塊標識符
  • CommonJS規範的特色

1. 暴露(定義)模塊

正確的暴露方式:

暴露模塊有兩種方式:

  • module.exports = {}
  • exports.xxx = 'xxx'

例若有一個 m1.js 文件:

第一種暴露方式:

module.exports = {  
    name: 'lindaidai',  
    sex: 'boy'  
}

第二種暴露方式:

exports.name = 'lindaidai';  
exports.sex = 'boy'

爲何能夠有這兩種寫法呢?

我是這樣理解的:module這個變量它表明的就是整個模塊,也就是m1.js。而其實這個module變量是有一個屬性exports的,它是一個叫作exports變量的引用,咱們能夠寫一下僞代碼:

var exports = {};  
var module = {  
    exports: exports  
}  
return module.exports

(固然這只是僞代碼啊,實際你這麼去用會發現沒有效果)

最後導出的是module.exports,而不是exports

容易混淆的暴露方式:

若是你在代碼中試圖 exports = { name: 'lindaidai' },你會發如今引入的地方根本獲取不到name屬性。

// m1.js  

exports = {  
    name: 'lindaidai'  
}
// test.js

const math = require('./m1.js')    
console.log(m1); // {}

在控制檯執行 node test.js,發現打印出來的 m1 是一個空的對象。

我是這樣理解的:整個模塊的導出是靠 module.exports 的,若是你從新對整個 exports 對象賦值的話,它和 module.exports 就不是同一個對象了,由於它們指向的引用地址都不一樣:

module.exports -> {} // 指向一個空的對象  
exports -> { name: 'lindaidai' } // 指向的是另外一個對象

因此你對 exports = {} 作任何操做都影響不到 module.exports

讓咱們來看幾個正確和錯誤的示例吧:

// m1.js

// 1. 正確  
module.exports = {  
    name: 'lindaidai',  
    sex: 'boy'  
}  
  
// 2. 正確  
exports.name = 'lindaidai';  
exports.sex = 'boy'  
  
// 3. 正確  
module.exports.name = 'lindaidai';  
module.exports.sex = 'boy'  
  
// 4. 無效  
exports = {  
    name: 'lindaidai',  
    sex: 'boy'  
}

能夠看到

  • exports.name = xxxmodule.exports.name = xxx 的縮寫。
  • exports = {} 卻不是 module.exports = {} 的縮寫。

2. 引用(引入)模塊

對於模塊的引用使用全局方法 require() 就能夠了。

注意⚠️這個全局方法是 node 中的方法哈,它不是 window 下面的,因此若是你沒作任何處理想直接在 html 裏用確定就是不行的了:

index.html:

<body>  
    <script>  
        var m1 = require('./m1.js')  
        console.log(m1);  
    </script>  
</body>

例如上面👆這樣你打開頁面控制檯確定就報錯了:

Uncaught ReferenceError: require is not defined  
    at index.html:11

而若是你是在另外一個 js 文件中引用(例如 test.js ),並在終端執行 node test.js 是能夠用的:

test.js:

var m1 = require('./m1.js')  
  
console.log(m1);

那是由於你的電腦上全局安裝了 Node.js,因此能夠這樣玩。

因此咱們能夠發現 require() 它是 Node.js 中的一個全局方法,並非CommonJS獨有的,CommonJS只是衆多規範中的其中一種。

這種規範容許咱們:

  • 使用 module.exports = {} 或者 exports.name = xxx 導出模塊
  • 使用 const m1 = require('./m1') 引入模塊

注意⚠️:

另外還有一點比較重要,那就是 require() 的參數甚至能容許你是一個表達式。

也就是說你能夠把它設置爲一個變量:

test.js:

var m1Url = './m1.js';  
var m1 = require(m1Url);  
  
// 甚至作一些字符串拼接:  
var m1 = require('./m' + '1.js');

3. 模塊標識符(標識)

模塊標識符其實就是你在引入模塊時調用 require() 函數的參數。

你會看到咱們常常會有這樣的用法:

// 直接導入  
const path = require('path');  
// 相對路徑  
const m1 = require('./m1.js');  
// 直接導入  
const lodash = require('lodash');

這實際上是由於咱們引入的模塊會有不一樣的分類,像path這種它是Node.js就自帶的模塊,m1是路徑模塊,lodash是咱們使用npm i lodash下載到node_modules裏的模塊。

分爲如下三種:

  • 核心模塊(Node.js自帶的模塊)
  • 路徑模塊(相對或絕對定位開始的模塊)
  • 自定義模塊(node_modules裏的模塊)

三種模塊的查找方式:

  • 核心模塊,直接跳過路徑分析和文件定位
  • 路徑模塊,直接得出相對路徑就行了
  • 自定義模塊,先在當前目錄的node_modules裏找這個模塊,若是沒有,它會往上一級目錄查找,查找上一級的node_modules,依次往上,直到根目錄下都沒有, 就拋出錯誤。

自定義模塊的查找過程:

這個過程其實也叫作路徑分析

如今我把剛剛的test.js來改一下:

// var m1 = require('./m1.js');  
  
// console.log(m1);  
console.log(module.paths)

而後在終端執行:

node test.js

會發現輸出了下面的一個數組:

LinDaiDaideMBP:commonJS lindaidai$ node test.js  
[  
  '/Users/lindaidai/codes/test/CommonJS和ES6/commonJS/node_modules',  
  '/Users/lindaidai/codes/test/CommonJS和ES6/node_modules',  
  '/Users/lindaidai/codes/test/node_modules',  
  '/Users/lindaidai/codes/node_modules',  
  '/Users/lindaidai/node_modules',  
  '/Users/node_modules',  
  '/node_modules'  
]

這裏所說的查找,是指查找你如今用的這個模塊,我如今用的是test.js,你可能看不出什麼效果。如今讓咱們來模擬一個咱們使用npm i安裝的一個自定義模塊功能。

首先,我在根目錄下新建了一個名叫node_modules的文件夾,並在其中新建了一個名叫lindaidai.js的文件,用來模擬一個npm安裝的依賴。

目錄結構:

稍微編寫一下lindaidai.js:

module.exports = {  
  print: function () {  
    console.log('lindaidai')  
  }  
}  
console.log('lindaidai模塊:', module.paths)

而後在test.js中引入這個lindaidai模塊:

// var m1 = require('./m1.js');  
// console.log(m1);  
// console.log(module.paths)  
  
var lindaidai = require('lindaidai');  
lindaidai.print();

如今執行node test.js,會發現輸出了:

LinDaiDaideMBP:commonJS lindaidai$ node test.js  
lindaidai模塊: [  
  '/Users/lindaidai/codes/test/CommonJS和ES6/commonJS/node_modules',  
  '/Users/lindaidai/codes/test/CommonJS和ES6/node_modules',  
  '/Users/lindaidai/codes/test/node_modules',  
  '/Users/lindaidai/codes/node_modules',  
  '/Users/lindaidai/node_modules',  
  '/Users/node_modules',  
  '/node_modules'  
]  
lindaidai

因此如今你能夠知道,日常咱們使用這種依賴的時候,它是怎樣的一個查找順序了吧,它其實就是按照自定義模塊的順序來進行查找。

文件定位:

上面👆已經介紹完了路徑分析,可是還有一個問題,就是咱們導入的模塊它的後綴(擴展名)是能夠省略的啊,那Node怎麼知道咱們是導入了一個js仍是一個json呢?這其實就涉及到了文件定位。

在NodeJS中, 省略了擴展名的文件, 會依次補充上.js, .node, .json來嘗試, 若是傳入的是一個目錄, 那麼NodeJS會把它當成一個包來看待, 會採用如下方式肯定文件名

第一步, 找出目錄下的package.json, 用JSON.parse()解析出main字段

第二步, 若是main字段指定的文件仍是省略了擴展, 那麼會依次補充.js, .node, .json嘗試.

第三步, 若是main字段制定的文件不存在, 或者根本就不存在package.json, 那麼會默認加載這個目錄下的index.js, index.node, index.json文件.

以上就是文件定位的過程, 再搭配上路徑分析的過程, 進行排列組合, 這得有多少種可能呀. 因此說, 自定義模塊的引入, 是最費性能的.

(總結來源:https://zhuanlan.zhihu.com/p/...

4. CommonJS規範的特色

我先把CommonJS規範的一些特色列舉出來吧,而後咱們再一點一點的去看例子。

  • 全部代碼都運行在模塊做用域,不會污染全局做用域;
  • 模塊是同步加載的,即只有加載完成,才能執行後面的操做;
  • 模塊在首次執行後就會緩存,再次加載只返回緩存結果,若是想要再次執行,可清除緩存;
  • CommonJS輸出是值的拷貝(即,require返回的值是被輸出的值的拷貝,模塊內部的變化也不會影響這個值)。

(總結來源:https://juejin.im/post/5db95e...

第一點仍是好理解的,咱模塊的一個重要的功能不就是這個嗎。

第二點同步加載,這個寫個案例咱們來驗證一下

同步加載案例

_m1.js_:

console.log('我是m1模塊')  
module.exports = {  
    name: 'lindaidai',  
    sex: 'boy'  
}

test.js

var m1 = require('./m1');  
console.log('我是test模塊');

能夠看到,test模塊依賴於m1,且是先下載的m1模塊,因此若是我執行node test.js,會有如下的執行結果:

LinDaiDaideMBP:commonJS lindaidai$ node test.js  
我是m1模塊  
我是test模塊

這也就驗證了CommonJS中,模塊是同步加載的,即只有加載完成,才能執行後面的操做。

第三點模塊首次執行後會緩存,咱們也能夠寫個案例來驗證一下。

模塊首次執行後會緩存案例:

_m1.js_:

var name = 'lindaidai';  
var sex = 'boy';  
  
exports.name = name;  
exports.sex = sex;

_test.js_:

var m1 = require('./m1');  
m1.sex = 'girl';  
console.log(m1);  
  
var m2 = require('./m1');  
console.log(m2);

test一樣依賴於m1,可是我會在其中導入兩次m1,第一次導入的時候修改了m1.sex的值,第二次的時候命名爲m2,可是結果m1m2居然是相等的:

LinDaiDaideMBP:commonJS lindaidai$ node test.js  
{ name: 'lindaidai', sex: 'girl' }  
{ name: 'lindaidai', sex: 'girl' }

也就是說模塊在首次執行後就會緩存,再次加載只返回緩存結果,這裏我是用了改變m1.sex的值來證實它確實是取了緩存結果。

那麼就有小夥伴會疑惑了,其實你這樣寫也並不能證實啊,由於你改變了m1.sex也多是影響本來m1模塊裏的sex屬性呀,這樣的話第二次m2拿到的確定就是被改變的值了。

唔...我正想證實來着呢。由於CommonJS的第四個特色就能夠很好的解決你這個疑問。

第四點CommonJS輸出是值的拷貝,也就是說你用require()引入了模塊,可是你在最新的模塊中怎樣去改變,也不會影響你已經require()的模塊。來看個案例。

CommonJS輸出是值的拷貝案例

_m1.js_:

var name = 'lindaidai';  
var sex = 'boy';  
var advantage = ['handsome']  
  
setTimeout(function () {  
  sex = 'girl';  
  advantage.push('cute');  
}, 500)  
  
exports.name = name;  
exports.sex = sex;  
exports.advantage = advantage;

_test.js_:

var m1 = require('./m1');  
setTimeout(function () {  
  console.log('read count after 1000ms in commonjs is', m1.sex)  
  console.log('read count after 1000ms in commonjs is', m1.advantage)  
}, 1000)

執行node test.js以後的執行結果是:

LinDaiDaideMBP:commonJS lindaidai$ node test.js  
read count after 1000ms in commonjs is boy  
read count after 1000ms in commonjs is [ 'handsome', 'cute' ]

也就是說,在開始var m1 = require('./m1')的時候,m1已經被引入進來了,可是過了500ms後我改變了本來m1裏的一些屬性,sex這種基本數據類型是不會被改變的,可是advantage這種引用類型共用的仍是同一個內存地址。(這種複製的關係讓我想到了以前學原型鏈繼承的時候,它那裏也是,會影響Father.prototype上的引用類型)

備註 其實這裏的拷貝是指 JavaScript 的淺拷貝,若是對於 JavaScript 的深淺拷貝有疑問,能夠參考 JavaScript 的淺拷貝和深拷貝

若是這裏你是這樣寫的話:

_m1.js_:

var name = 'lindaidai';  
var sex = 'boy';  
var advantage = ['handsome']  
  
setTimeout(function () {  
  sex = 'girl';  
  // advantage.push('cute');  
  advantage = ['cute'];  
}, 500)  
  
exports.name = name;  
exports.sex = sex;  
exports.advantage = advantage;

如今的執行結果確定就是:

LinDaiDaideMBP:commonJS lindaidai$ node test.js  
read count after 1000ms in commonjs is boy  
read count after 1000ms in commonjs is [ 'handsome' ]

由於至關於對m1advantage從新賦值了。

固然,或者若是你的m1.js中返回的值是會有一個函數的話,在test.js也能拿到變化以後的值了,好比這裏的一個例子:

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

由於在這裏實際就造成了一個閉包,而counter屬性就是一個取值器函數。

好滴,這基本就是CommonJS的特色了,總結就不寫了,在開頭已經說過了,不過對於最後一點:CommonJS輸出是值的拷貝,這個對於引用類型的變量來講仍是會有一點歧義的,好比上面的advantage那個例子,你們知道就好了。

AMD規範

1. 產生緣由

上面介紹的CommonJS規範看起來挺好用的啊,爲何又還要有其它的規範呢?好比AMD、CMD,那它們和CommonJS又有什麼淵源呢?

咱們知道,模塊化這種概念不只僅適用於服務器端,客戶端一樣也適用。

CommonJS規範就不太適合用在客戶端(瀏覽器)環境了,好比上面的那個例子,也就是:

test.js:

const m1 = require('./m1.js')  
console.log(m1);  
  
// 與m1模塊無關的一些代碼  
function other () {}  
other();

這段代碼放在瀏覽器環境中,它會如何運行呢?

  • 首先加載m1.js
  • m1.js加載完畢以後才執行後面的內容

這點其實在CommonJS規範的特色中已經提到過了。

後面的內容要等待m1加載完纔會執行,若是m1加載的很慢呢?那不就形成了卡頓,這對於客戶端來講確定是不友好的。像這種要等待上一個加載完才執行後面內容的狀況咱們能夠叫作"同步加載",很顯然,這裏咱們更但願的是other()的執行不須要等m1加載完才執行,也就是咱們但願m1它是"異步加載"的,這也就是AMD

在介紹AMD以前讓咱們看看CommonJS規範對服務器端和瀏覽器的不一樣,它有助於讓你理解爲何說CommonJS不太適合於客戶端:

  • 服務器端全部的模塊都存放在本地硬盤中,能夠同步加載完成,等待時間就是硬盤的讀取時間。
  • 瀏覽器,全部的模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於假死狀態。

2. 定義並暴露模塊

有了上面這層背景,咱們就知道了,AMD它的產生很大一部分緣由就是爲了能讓咱們採用異步的方式加載模塊

因此如今來讓咱們看看它的介紹吧。

AMDAsynchronous Module Definition的縮寫,也就是"異步模塊定義"。(前面的A就很好記了,它讓我不自覺的就想到async這個定義異步函數的修飾符)

它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行。

此時就須要另外一個重要的方法來定義咱們的模塊:define()

它實際上是會有三個參數:

define(id?, dependencies?, factory)

  • id: 一個字符串,表示模塊的名稱,可是是可選的
  • dependencies: 一個數組,是咱們當前定義的模塊要依賴於哪些模塊,數組中的每一項表示的是要依賴模塊的相對路徑,且這個參數也是可選的
  • factory: 工廠方法,一個函數,這裏面就是具體的模塊內容了

坑一

那其實就有一個問題了,看了這麼多的教材,但我想要去寫案例的時候,我覺得這個define能直接像require同樣去用,結果發現控制檯一直再報錯:

ReferenceError: define is not defined

看來它還並非Node.js自帶的一個方法啊,搜尋了一下,原來它只是名義上規定的這樣一個方法,可是你真的想要去用仍是得使用對應的JavaScript庫,也就是咱們經常聽到的:

目前,主要有兩個Javascript庫實現了AMD規範:require.js和curl.js。

我酸了...

讓咱們去requirejs的官網看看如何使用它,因爲個人案例都是在Node執行環境中,因而我採用npm install的方式來下載了:

我新建了一個叫AMD的文件夾,做爲AMD的案例。

在項目的根目錄下執行:

npm i requirejs

(找了一圈NPM也沒看到能使用CDN遠程引入的)

執行完畢以後,項目的根目錄下出現了依賴包,打開看了看,確實是下載下來了:

如今能夠開心的在項目裏用define()了 😊。

來看個小例子,我從新定義了一個math.js

math.js

define(function () {  
  var add = function (a, b) {  
    return a + b;  
  }  
  return {  
    add: add  
  }  
})

這裏模塊很簡單,導出了一個加法函數。

(至於這裏爲何add: add要這樣寫,而不是隻簡寫爲add呢?別忘了這種對象同名屬性簡寫是ES6纔出來的哦)

3. 引用模塊

坑二

OK👌,既然模塊已經能導出了,那就讓咱們來看看如何引用吧,依照着教材,我在test.js中引入了math模塊並想要調用add()方法:

test.js:

require(['math'],function(math) {  
  console.log(math)  
  console.log(math.add(1, 2));  
})

以後熟練的執行node test.js

我酸了...

又報錯了,擦...

throw new ERR_INVALID_ARG_TYPE(name, 'string', value);  
 TypeError [ERR_INVALID_ARG_TYPE]: The "id" argument must be of type string. Received an instance of Array

確認了一下,和教材們中的寫法同樣啊,第一個參數爲要加載的模塊數組,第二個參數爲加載完以後的回調。

難受😣...原來上面👆require([modules], callback)這樣的寫法它和define同樣都只是個噱頭,若是你真得用的話,仍是得用JavaScript庫中的方法。

因爲上面已經安裝過requirejs了,這裏我直接使用就能夠了,如今我修改了一下test.js文件:

var requirejs = require("requirejs"); //引入requirejs模塊  
  
requirejs(['math'],function(math) {  
  console.log(math)  
  console.log(math.add(1, 2));  
})

好了,如今執行node test.js就能夠正常使用了...

(很難受...感受明明已是很常見耳熟能詳的一些知識了,真的要去用的時候發現和不少教材中說的不是那麼一回事...也但願你們在看完了一些教材以後最好能親自去實踐一下,由於本身也是寫博客的,因此也知道有些時候一些知識點可能也是從別人的文章那裏看來可是沒有通過實踐的,因此最好也仍是本身動動手)

4. 依賴其它模塊的define

能夠看到define它還有另外兩個參數的,第一個是模塊的名稱,沒啥好說的,讓咱們來看看第二個它所依賴的模塊。

還記得在CommonJS規範那裏咱們寫了一個m1.js嗎?如今就讓咱們把這個模塊拿來用下,把它做爲math.js中的一個依賴。

m1.js:

console.log('我是m1, 我被加載了...')  
module.exports = {  
    name: 'lindaidai',  
    sex: 'boy'  
}

而後修改一下math.js

math.js:

define(['m1'], function (m1) {  
  console.log('我是math, 我被加載了...')  
  var add = function (a, b) {  
    return a + b;  
  }  
  var print = function () {  
    console.log(m1.name)  
  }  
  return {  
    add: add,  
    print: print  
  }  
})

另外,爲了方便你們看,咱們再來修改一下剛剛的test.js

var requirejs = require("requirejs"); //引入requirejs模塊  
  
requirejs(['math'],function(math) {  
  console.log('我是test, 我被加載了...')  
  console.log(math.add(1, 2));  
  math.print();  
})  
function other () {  
  console.log('我是test模塊內的, 可是我不依賴math')  
};  
other();

因此咱們能夠看到,依賴關係依次爲:

test -> math -> m1

若是按照AMD的規範,模塊的加載須要依靠前一個模塊加載完纔會執行回調函數內的內容,那麼咱們能夠想象當我在終端輸入node test.js的時候,要出現的結果應該是:

LinDaiDaideMBP:commonJS lindaidai$ node test.js  
我是test模塊內的, 可是我不依賴math  
我是m1, 我被加載了...  
我是math, 我被加載了...  
我是test, 我被加載了...  
3  
lindaidai

(這個,相信你們應該都看清了彼此的依賴關係吧😢)

可是現實老是那麼的殘酷,當我按下回車的時候,又報錯了...

再酸...

 ReferenceError: module is not defined

看了一下這個報錯的內容,是在m1.js中...呆了幾秒鐘反應了過來...

既然是使用AMD的規範,那咱們確定是要一統到底了,m1.js中用的仍是CommonJS的規範,固然不行了。

OK,來修改一下m1.js

m1.js:

define(function () {  
  console.log('我是m1, 我被加載了...')  
  return {  
    name: 'lindaidai',  
    sex: 'boy'  
  }  
})

OK👌,此次沒啥問題了,按照咱們預期的去執行了...😊。

(固然據個人瞭解,requirejs還可用於在script中引用而後定義網頁程序的主模塊等使用,能夠看一下:

http://www.ruanyifeng.com/blo..._js.html)

AMD的知識點大概就介紹到了這裏,相信你們也知道它的基本使用了吧,至於其中的一些區別什麼的我在最後也會列一份清單,不過如今讓咱們先來看看CMD吧。

CMD規範

CMD (Common Module Definition), 是seajs推崇的規範,依賴就近,用的時候再require。

來看段代碼,大概感覺一下它是怎樣用的:

define(function(require, exports, module) {  
  var math = require('./math');  
  math.print()  
})

看着和AMD有點像的,沒錯,其實define()的參數甚至都是同樣的:

define(id?, dependencies?, factory)

可是區別在於哪裏呢?讓咱們來看看最後一個factory它參數。

factory函數中是會接收三個參數:

  • require
  • exports
  • module

    這三個很好理解,對應着以前的CommonJS那不就是:

  • require:引入某個模塊
  • exports:當前模塊的exports,也就是module.exports的簡寫
  • module:當前這個模塊

如今再來講說AMDCMD的區別。

雖然它們的define()方法的參數都相同,可是:

  • AMD中會把當前模塊的依賴模塊放到dependencies中加載,並在factory回調中拿到加載成功的依賴
  • CMD通常不在dependencies中加載,而是寫在factory中,使用require加載某個依賴模塊

所以纔有了咱們經常看到的一句話:

AMD和CMD最大的區別是對依賴模塊的執行時機處理不一樣,注意不是加載的時機或者方式不一樣,兩者皆爲異步加載模塊。

(好吧,仔細讀了2遍感受仍是沒太明白,沒事,後面呆呆還會詳細說到)

比較有名一點的,seajs,來看看它推薦的CMD 模塊書寫格式吧:

// 全部模塊都經過 define 來定義  
define(function(require, exports, module) {  
  
  // 經過 require 引入依賴  
  var $ = require('jquery');  
  var Spinning = require('./spinning');  
  
  // 經過 exports 對外提供接口  
  exports.doSomething = ...  
  
  // 或者經過 module.exports 提供整個接口  
  module.exports = ...  
  
});

這是官網的一個小案例,我也去seajs的文檔中看了一下沒啥太大問題,這裏就不舉例了。

AMD和CMD的區別

AMD和CMD最大的區別是對依賴模塊的執行時機處理不一樣,注意不是加載的時機或者方式不一樣,兩者皆爲異步加載模塊。

仍是上面那句話,讓咱們來看個小例子理解一下。

一樣是math模塊中須要加載m1模塊。

AMD中咱們會這樣寫:

math.js

define(['m1'], function (m1) {  
  console.log('我是math, 我被加載了...')  
  var add = function (a, b) {  
    return a + b;  
  }  
  var print = function () {  
    console.log(m1.name)  
  }  
  return {  
    add: add,  
    print: print  
  }  
})

可是對於CMD,咱們會這樣寫:

math.js

define(function (require, exports, module) {  
  console.log('我是math, 我被加載了...')  
  var m1 = require('m1');  
  var add = function (a, b) {  
    return a + b;  
  }  
  var print = function () {  
    console.log(m1.name)  
  }  
  module.exports = {  
    add: add,  
    print: print  
  }  
})

假如此時m1.js中有一個語句是在m1模塊被加載的時候打印出"我是m1, 我被加載了..."

執行結果區別:

  • AMD,會先加載m1"我是m1"會先執行
  • CMD,我是"我是math"會先執行,由於本題中console.log('我是math, 我被加載了...')是放在require('m1')前面的。

如今能夠很明顯的看到區別了。

AMD依賴前置,js很方便的就知道要加載的是哪一個模塊了,由於已經在definedependencies參數中就定義好了,會當即加載它。

CMD是就近依賴,也就是說模塊的回調函數執行到加載語句時纔會去加載。

OK👌,來看個總結:

二者之間,最明顯的區別就是在模塊定義時對依賴的處理不一樣

一、AMD推崇依賴前置,在定義模塊的時候就要聲明其依賴的模塊二、CMD推崇就近依賴,只有在用到某個模塊的時候再去require

ES6 Modules規範

ES6標準出來後,ES6 Modules規範算是成爲了前端的主流吧,以import引入模塊,export導出接口被愈來愈多的人使用。

下面,我也會從這麼幾個方面來介紹ES6 Modules規範:

export命令和import命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,這是由於處於條件代碼塊之中,就無法作靜態優化了,違背了ES6模塊的設計初衷。

1. export導出模塊

export有兩種模塊導出方式:

  • 命名式導出(名稱導出)
  • 默認導出(自定義導出)

命名式導出

來看幾種正確和錯誤的寫法吧:

// 如下兩種爲錯誤  
// 1.  
export 1;  
// 2.  
const a = 1;  
export a;  
  
// 如下爲正確  
// 3.  
const a = 1;  
export { a };  
  
// 4. 接口名與模塊內部變量之間,創建了一一對應的關係  
export const a = 1, b = 2;  
  
// 5. 接口名與模塊內部變量之間,創建了一一對應的關係  
export const a = 1;  
export const b = 2;  
  
// 或者用 as 來命名  
const a = 1;  
export { a as outA };  
  
const a = 1;  
const b = 2;  
export { a as outA, b as outB };

容易混淆的多是24兩種寫法了,看着很像,可是2卻不行。2直接導出一個值爲1的變量是和狀況一同樣,沒有什麼意義,由於你在後面要用的時候並不能完成解構。

可是4中,接口名與模塊內部變量之間,創建了一一對應的關係,因此能夠。

默認導出

默認導出會在export後面加上一個default

// 1.  
const a = 1;  
export default a;  
  
// 2.  
const a = 1;  
export default { a };  
  
// 3.  
export default function() {}; // 能夠導出一個函數  
export default class(){}; // 也能夠出一個類

其實,默認導出能夠理解爲另外一種形式上的命名導出,也就是說a這個屬性名至關因而被我重寫了成了default

const a = 1;  
export defalut a;  
// 等價於  
export { a as default }

因此,咱們才能夠用const a = 1; export default a;這種方式導出一個值。

2. import導入模塊

import模塊導入與export模塊導出功能相對應,也存在兩種模塊導入方式:命名式導入(名稱導入)和默認導入(定義式導入)。

來看看寫法:

// 某個模塊的導出 moudule.js  
export const a = 1;  
  
// 模塊導入  
// 1. 這裏的a得和被加載的模塊輸出的接口名對應  
import { a } from './module'  
  
// 2. 使用 as 換名  
import { a as myA } from './module'  
  
// 3. 如果只想要運行被加載的模塊能夠這樣寫,可是即便加載2次也只是運行一次  
import './module'  
  
// 4. 總體加載  
import * as module from './module'  
  
// 5. default接口和具名接口  
import module, { a } from './module'

第四種寫法會獲取到module中全部導出的東西,而且賦值到module這個變量下,這樣咱們就能夠用module.a這種方式來引用a了。

3. export ... from...

其實還有一種寫法,能夠將exportfrom結合起來用。

例如,我有三個模塊a、b、c

c模塊如今想要引入a模塊,可是它不不直接引用a,而是經過b模塊來引用,那麼你可能會想到b應該這樣寫:

import { someVariable } from './a';  
  
export { someVariable };

引入someVariable而後再導出。

這還只是一個變量,咱們得導入再導出,如果有不少個變量須要這樣,那無疑會增長不少代碼量。

因此這時候能夠用下面這種方式來實現:

export { someVariable } from './a';

不過這種方式有一點須要注意:

  • 這樣的方式不會將數據添加到該聚合模塊的做用域, 也就是說, 你沒法在該模塊(也就是b)中使用someVariable

4. ES6 Modules規範的特色

總結一下它的特色哈:

  • 輸出使用export
  • 輸入使用import
  • 可使用export...from...這種寫法來達到一個"中轉"的效果
  • 輸入的模塊變量是不可從新賦值的,它只是個可讀引用,不過卻能夠改寫屬性
  • export命令和import命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,這是由於處於條件代碼塊之中,就無法作靜態優化了,違背了ES6模塊的設計初衷。
  • import命令具備提高效果,會提高到整個模塊的頭部,首先執行。

5. Bable下的ES6模塊轉換

還有一點就是,若是你有使用過一些ES6的Babel的話,你會發現當使用export/import的時候,Babel也會把它轉換爲exports/require的形式。

例如個人輸出:

_m1.js_:

export const count = 0;

個人輸入:

_index.js_:

import {count} from './m1.js'  
console.log(count)

當使用Babel編譯以後,各自會被轉換爲:

_m1.js_:

"use strict";  
  
Object.defineProperty(exports, "__esModule", {  
  value: true  
});  
exports.count = void 0;  
const count = 0;  
exports.count = count;

_index.js_:

"use strict";  
  
var _m = require("./m1.js");  
  
console.log(_m.count);

正是由於這種轉換關係,才能讓咱們把exportsimport結合起來用:

也就是說你能夠這樣用:

// 輸出模塊 m1.js  
exports.count = 0;  
  
  
// index.js中引入  
import {count} from './m1.js'  
console.log(count)

CommonJS與ES6 Modules規範的區別

😂,我相信不少人就比較關心它兩區別的問題,由於基本上面試問的就是這個。好吧,這裏來作一個算是比較詳細的總結吧。

  • CommonJS模塊是運行時加載,ES6 Modules是編譯時輸出接口
  • CommonJS輸出是值的淺拷貝;ES6 Modules輸出的是值的引用,被輸出模塊的內部的改變會影響引用的改變
  • CommonJs導入的模塊路徑能夠是一個表達式,由於它使用的是require()方法;而ES6 Modules只能是字符串
  • CommonJSthis指向當前模塊,ES6 Modulesthis指向undefined
  • 且ES6 Modules中沒有這些頂層變量:argumentsrequiremoduleexports__filename__dirname

關於第一個差別,是由於CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

(應該還一些區別我沒想到的,歡迎補充👏😊)

參考文章

知識無價,支持原創。

參數文章:

  • 《這幾個概念你可能仍是沒搞清require、import和export》
  • 《前端模塊化,AMD與CMD的區別》
  • 《node.js中使用define和require》
  • 《必需要知道的CommonJS和ES6 Modules規範》
  • 《再次梳理AMD、CMD、CommonJS、ES6 Module的區別》
  • 《阮一峯-Module 的加載實現》
相關文章
相關標籤/搜索