模塊規(guī)范CommonJS以及AMD作郭、CMD

本文目錄:

  • 1.原始寫法
  • 2.CommonJS規(guī)范
  • 3.AMD規(guī)范
  • 4.CMD規(guī)范
  • 5.AMD和CMD的區(qū)別
  • 6.ES6 Modules規(guī)范
  • 7.CommonJS與ES6 Modules規(guī)范的區(qū)別
  • 8.拓展

開發(fā)的時(shí)候,我們經(jīng)常會(huì)把某些功能封裝成可復(fù)用的模塊。模塊封裝了功能,并且對外暴露一個(gè)API揍愁。

1.原始寫法

1. 一個(gè)函數(shù)就是一個(gè)模塊

function m1 () {
  // ...
}

缺點(diǎn):污染了全局變量拾氓,無法保證不會(huì)與其它模塊發(fā)生沖突冯挎,而且模塊成員之間看不出直接關(guān)系。
2. 一個(gè)對象就是一個(gè)模塊
對象寫法 為了解決上面的缺點(diǎn)咙鞍,可以把模塊寫成一個(gè)對象房官,所有的模塊成員都放到這個(gè)對象里面。

var module1 = new Object({
  _sum: 0,
  foo1: function () {},
  foo2: function () {}
})

缺點(diǎn):會(huì)暴露所有模塊成員续滋,內(nèi)部的狀態(tài)可能被改寫翰守。
例如,我們?nèi)绻皇窍氡┞冻鰞蓚€(gè)方法而不暴露出_sum疲酌,就做不到蜡峰。
而此時(shí),_sum可能被外部改寫:
module1._sum = 2;
3. 立即執(zhí)行函數(shù)為一個(gè)模塊

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

利用立即執(zhí)行函數(shù)內(nèi)的作用域已經(jīng)閉包來實(shí)現(xiàn)模塊功能朗恳,導(dǎo)出我們想要導(dǎo)出的成員湿颅。
此時(shí)外部代碼就不能讀取到_sum了:
console.log(module1._sum) // undefined
隨著Node.js的誕生和發(fā)展,JavaScript可以在服務(wù)端運(yùn)行粥诫,同時(shí)客戶端應(yīng)用也越來越流行油航,JavaScript界產(chǎn)生了對優(yōu)秀模塊系統(tǒng)的需求。在JavaScript中定義模塊的規(guī)范也隨之產(chǎn)生怀浆。

2.CommonJS規(guī)范

1. 暴露(定義)模塊
正確的暴露方式:
暴露模塊有兩種方式:

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

例如有一個(gè)m1.js文件:
第一種暴露方式:

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

第二種暴露方式:

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

為什么可以有這兩種寫法呢谊囚?
我是這樣理解的:module這個(gè)變量它代表的就是整個(gè)模塊,也就是m1.js执赡。而其實(shí)這個(gè)module變量是有一個(gè)屬性exports的镰踏,它是一個(gè)叫做exports變量的引用,我們可以寫一下偽代碼:

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

(當(dāng)然這只是偽代碼啊搀玖,實(shí)際你這么去用會(huì)發(fā)現(xiàn)沒有效果)
最后導(dǎo)出的是module.exports余境,而不是exports。
容易混淆的暴露方式:
如果你在代碼中試圖exports = { name: 'lindaidai' }灌诅,你會(huì)發(fā)現(xiàn)在引入的地方根本獲取不到name屬性芳来。

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

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

在控制臺(tái)執(zhí)行node test.js,發(fā)現(xiàn)打印出來的m1是一個(gè)空的對象猜拾。
我是這樣理解的:整個(gè)模塊的導(dǎo)出是靠module.exports的即舌,如果你重新對整個(gè)exports對象賦值的話,它和module.exports就不是同一個(gè)對象了挎袜,因?yàn)樗鼈冎赶虻囊玫刂范疾煌?br> module.exports -> {} // 指向一個(gè)空的對象
exports -> { name: 'lindaidai' } // 指向的是另一個(gè)對象
所以你對exports = {}做任何操作都影響不到module.exports顽聂。
讓我們來看幾個(gè)正確和錯(cuò)誤的示例吧:

// 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 = xxx是module.exports.name = xxx的縮寫肥惭。
  • exports = {}卻不是module.exports = {}的縮寫。

2. 引用(引入)模塊
對于模塊的引用使用全局方法require()就可以了紊搪。
注意??這個(gè)全局方法是node中的方法哈蜜葱,它不是window下面的,所以如果你沒做任何處理想直接在html里用肯定就是不行的了:
index.html:

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

例如上面這樣你打開頁面控制臺(tái)肯定就報(bào)錯(cuò)了:
Uncaught ReferenceError: require is not defined at index.html:11
而如果你是在另一個(gè)js文件中引用(例如test.js)耀石,并在終端執(zhí)行node test.js是可以用的:
test.js:

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

那是因?yàn)槟愕碾娔X上全局安裝了Node.js牵囤,所以可以這樣玩。
所以我們可以發(fā)現(xiàn)require()它是Node.js中的一個(gè)全局方法滞伟,并不是CommonJS獨(dú)有的揭鳞,CommonJS只是眾多規(guī)范中的其中一種。
這種規(guī)范允許我們:

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

注意:
另外還有一點(diǎn)比較重要梆奈,那就是require()的參數(shù)甚至能允許你是一個(gè)表達(dá)式野崇。
也就是說你可以把它設(shè)置為一個(gè)變量:
test.js:

var m1Url = './m1.js';
var m1 = require(m1Url);

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

3. 模塊標(biāo)識(shí)符(標(biāo)識(shí))
模塊標(biāo)識(shí)符其實(shí)就是你在引入模塊時(shí)調(diào)用require()函數(shù)的參數(shù)。
你會(huì)看到我們經(jīng)常會(huì)有這樣的用法:

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

這其實(shí)是因?yàn)槲覀円氲哪K會(huì)有不同的分類亩钟,像path這種它是Node.js就自帶的模塊乓梨,m1是路徑模塊,lodash是我們使用npm i lodash下載到node_modules里的模塊径荔。
分為以下三種:

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

三種模塊的查找方式:

  • 核心模塊督禽,直接跳過路徑分析和文件定位
  • 路徑模塊,直接得出相對路徑就好了
  • 自定義模塊总处,先在當(dāng)前目錄的node_modules里找這個(gè)模塊,如果沒有睛蛛,它會(huì)往上一級(jí)目錄查找鹦马,查找上一級(jí)的node_modules,依次往上忆肾,直到根目錄下都沒有, 就拋出錯(cuò)誤荸频。

自定義模塊的查找過程:
這個(gè)過程其實(shí)也叫做路徑分析。
現(xiàn)在我把剛剛的test.js來改一下:

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

然后在終端執(zhí)行:
node test.js
會(huì)發(fā)現(xiàn)輸出了下面的一個(gè)數(shù)組:

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'
]

這里所說的查找客冈,是指查找你現(xiàn)在用的這個(gè)模塊旭从,我現(xiàn)在用的是test.js,你可能看不出什么效果〕≈伲現(xiàn)在讓我們來模擬一個(gè)我們使用npm i安裝的一個(gè)自定義模塊功能和悦。
首先,我在根目錄下新建了一個(gè)名叫node_modules的文件夾渠缕,并在其中新建了一個(gè)名叫l(wèi)indaidai.js的文件鸽素,用來模擬一個(gè)npm安裝的依賴。
目錄結(jié)構(gòu):


目錄結(jié)構(gòu)示例圖.png

稍微編寫一下lindaidai.js:

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

然后在test.js中引入這個(gè)lindaidai模塊:

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

現(xiàn)在執(zhí)行node test.js亦鳞,會(huì)發(fā)現(xiàn)輸出了:

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

所以現(xiàn)在你可以知道馍忽,平常我們使用這種依賴的時(shí)候棒坏,它是怎樣的一個(gè)查找順序了吧,它其實(shí)就是按照自定義模塊的順序來進(jìn)行查找遭笋。
文件定位:
上面已經(jīng)介紹完了路徑分析坝冕,但是還有一個(gè)問題,就是我們導(dǎo)入的模塊它的后綴(擴(kuò)展名)是可以省略的啊瓦呼,那Node怎么知道我們是導(dǎo)入了一個(gè)js還是一個(gè)json呢徽诲?這其實(shí)就涉及到了文件定位。
在NodeJS中, 省略了擴(kuò)展名的文件, 會(huì)依次補(bǔ)充上.js, .node, .json來嘗試, 如果傳入的是一個(gè)目錄, 那么NodeJS會(huì)把它當(dāng)成一個(gè)包來看待, 會(huì)采用以下方式確定文件名
第一步, 找出目錄下的package.json, 用JSON.parse()解析出main字段
第二步, 如果main字段指定的文件還是省略了擴(kuò)展, 那么會(huì)依次補(bǔ)充.js, .node, .json嘗試.
第三步, 如果main字段制定的文件不存在, 或者根本就不存在package.json, 那么會(huì)默認(rèn)加載這個(gè)目錄下的index.js, index.node, index.json文件.
以上就是文件定位的過程, 再搭配上路徑分析的過程, 進(jìn)行排列組合, 這得有多少種可能呀. 所以說, 自定義模塊的引入, 是最費(fèi)性能的.

4. CommonJS規(guī)范的特點(diǎn)

  • 所有代碼都運(yùn)行在模塊作用域吵血,不會(huì)污染全局作用域谎替;
  • 模塊是同步加載的,即只有加載完成蹋辅,才能執(zhí)行后面的操作钱贯;
  • 模塊在首次執(zhí)行后就會(huì)緩存,再次加載只返回緩存結(jié)果侦另,如果想要再次執(zhí)行秩命,可清除緩存;
  • CommonJS輸出是值的拷貝(即褒傅,require返回的值是被輸出的值的拷貝弃锐,模塊內(nèi)部的變化也不會(huì)影響這個(gè)值)。

第一點(diǎn)還是好理解的殿托,咱模塊的一個(gè)重要的功能不就是這個(gè)嗎霹菊。
第二點(diǎn)同步加載,這個(gè)寫個(gè)案例我們來驗(yàn)證一下支竹。
同步加載案例:
m1.js:

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

test.js

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

可以看到旋廷,test模塊依賴于m1,且是先下載的m1模塊礼搁,所以如果我執(zhí)行node test.js饶碘,會(huì)有以下的執(zhí)行結(jié)果:

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

這也就驗(yàn)證了CommonJS中,模塊是同步加載的馒吴,即只有加載完成扎运,才能執(zhí)行后面的操作。
第三點(diǎn)模塊首次執(zhí)行后會(huì)緩存饮戳,我們也可以寫個(gè)案例來驗(yàn)證一下豪治。
模塊首次執(zhí)行后會(huì)緩存案例:
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,但是我會(huì)在其中導(dǎo)入兩次m1莹捡,第一次導(dǎo)入的時(shí)候修改了m1.sex的值鬼吵,第二次的時(shí)候命名為m2,但是結(jié)果m1和m2竟然是相等的:

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

也就是說模塊在首次執(zhí)行后就會(huì)緩存篮赢,再次加載只返回緩存結(jié)果齿椅,這里我是用了改變m1.sex的值來證明它確實(shí)是取了緩存結(jié)果琉挖。
那么就有小伙伴會(huì)疑惑了,其實(shí)你這樣寫也并不能證明啊涣脚,因?yàn)槟愀淖兞薽1.sex也可能是影響原本m1模塊里的sex屬性呀示辈,這樣的話第二次m2拿到的肯定就是被改變的值了。
唔...我正想證明來著呢遣蚀。因?yàn)镃ommonJS的第四個(gè)特點(diǎn)就可以很好的解決你這個(gè)疑問矾麻。
第四點(diǎn)CommonJS輸出是值的拷貝,也就是說你用require()引入了模塊芭梯,但是你在最新的模塊中怎樣去改變险耀,也不會(huì)影響你已經(jīng)require()的模塊。來看個(gè)案例玖喘。
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)

執(zhí)行node test.js之后的執(zhí)行結(jié)果是:

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')的時(shí)候,m1已經(jīng)被引入進(jìn)來了累奈,但是過了500ms后我改變了原本m1里的一些屬性贬派,sex這種基本數(shù)據(jù)類型是不會(huì)被改變的,但是advantage這種引用類型共用的還是同一個(gè)內(nèi)存地址澎媒。(這種復(fù)制的關(guān)系讓我想到了之前學(xué)原型鏈繼承的時(shí)候搞乏,它那里也是,會(huì)影響Father.prototype上的引用類型)
如果這里你是這樣寫的話:
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;

現(xiàn)在的執(zhí)行結(jié)果肯定就是:

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

因?yàn)橄喈?dāng)于對m1的advantage重新賦值了戒努。
當(dāng)然请敦,或者如果你的m1.js中返回的值是會(huì)有一個(gè)函數(shù)的話,在test.js也能拿到變化之后的值了柏卤,比如這里的一個(gè)例子:

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

因?yàn)樵谶@里實(shí)際就形成了一個(gè)閉包冬三,而counter屬性就是一個(gè)取值器函數(shù)。
好滴缘缚,這基本就是CommonJS的特點(diǎn)了,總結(jié)就不寫了敌蚜,在開頭已經(jīng)說過了桥滨,不過對于最后一點(diǎn):CommonJS輸出是值的拷貝,這個(gè)對于引用類型的變量來說還是會(huì)有一點(diǎn)歧義的弛车,比如上面的advantage那個(gè)例子齐媒,大家知道就行了。

3.AMD規(guī)范

1. 產(chǎn)生原因
上面介紹的CommonJS規(guī)范看起來挺好用的啊纷跛,為什么又還要有其它的規(guī)范呢喻括?比如AMD、CMD贫奠,那它們和CommonJS又有什么淵源呢唬血?
我們知道望蜡,模塊化這種概念不僅僅適用于服務(wù)器端,客戶端同樣也適用拷恨。
而CommonJS規(guī)范就不太適合用在客戶端(瀏覽器)環(huán)境了脖律,比如上面的那個(gè)例子,也就是:
test.js:

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

這段代碼放在瀏覽器環(huán)境中腕侄,它會(huì)如何運(yùn)行呢小泉?

  • 首先加載m1.js
  • 等m1.js加載完畢之后才執(zhí)行后面的內(nèi)容

這點(diǎn)其實(shí)在CommonJS規(guī)范的特點(diǎn)中已經(jīng)提到過了。
后面的內(nèi)容要等待m1加載完才會(huì)執(zhí)行冕杠,如果m1加載的很慢呢微姊?那不就造成了卡頓,這對于客戶端來說肯定是不友好的分预。像這種要等待上一個(gè)加載完才執(zhí)行后面內(nèi)容的情況我們可以叫做"同步加載"兢交,很顯然,這里我們更希望的是other()的執(zhí)行不需要等m1加載完才執(zhí)行噪舀,也就是我們希望m1它是"異步加載"的魁淳,這也就是AMD。
在介紹AMD之前讓我們看看CommonJS規(guī)范對服務(wù)器端和瀏覽器的不同与倡,它有助于讓你理解為什么說CommonJS不太適合于客戶端:

  • 服務(wù)器端所有的模塊都存放在本地硬盤中界逛,可以同步加載完成,等待時(shí)間就是硬盤的讀取時(shí)間纺座。
  • 瀏覽器息拜,所有的模塊都放在服務(wù)器端,等待時(shí)間取決于網(wǎng)速的快慢净响,可能要等很長時(shí)間少欺,瀏覽器處于”假死”狀態(tài)。

2. 定義并暴露模塊
有了上面這層背景馋贤,我們就知道了赞别,AMD它的產(chǎn)生很大一部分原因就是為了能讓我們采用異步的方式加載模塊。
所以現(xiàn)在來讓我們看看它的介紹吧配乓。
AMD是Asynchronous Module Definition的縮寫仿滔,也就是"異步模塊定義"。(前面的A就很好記了犹芹,它讓我不自覺的就想到async這個(gè)定義異步函數(shù)的修飾符)

它采用異步方式加載模塊崎页,模塊的加載不影響它后面語句的運(yùn)行。所有依賴這個(gè)模塊的語句腰埂,都定義在一個(gè)回調(diào)函數(shù)中飒焦,等到加載完成之后,這個(gè)回調(diào)函數(shù)才會(huì)運(yùn)行屿笼。

此時(shí)就需要另一個(gè)重要的方法來定義我們的模塊:define()牺荠。
它其實(shí)是會(huì)有三個(gè)參數(shù):
define(id?, dependencies?, factory)

  • id: 一個(gè)字符串翁巍,表示模塊的名稱,但是是可選的
  • dependencies: 一個(gè)數(shù)組志电,是我們當(dāng)前定義的模塊要依賴于哪些模塊曙咽,數(shù)組中的每一項(xiàng)表示的是要依賴模塊的相對路徑,且這個(gè)參數(shù)也是可選的
  • factory: 工廠方法挑辆,一個(gè)函數(shù)例朱,這里面就是具體的模塊內(nèi)容了

坑一:
那其實(shí)就有一個(gè)問題了,看了這么多的教材鱼蝉,但我想要去寫案例的時(shí)候洒嗤,我以為這個(gè)define能直接像require一樣去用,結(jié)果發(fā)現(xiàn)控制臺(tái)一直再報(bào)錯(cuò):
ReferenceError: define is not defined
看來它還并不是Node.js自帶的一個(gè)方法啊魁亦,搜尋了一下渔隶,原來它只是名義上規(guī)定的這樣一個(gè)方法,但是你真的想要去用還是得使用對應(yīng)的JavaScript庫洁奈,也就是我們常常聽到的:

目前间唉,主要有兩個(gè)Javascript庫實(shí)現(xiàn)了AMD規(guī)范:require.js和curl.js。

讓我們?nèi)equirejs的官網(wǎng)看看如何使用它利术,由于我的案例都是在Node執(zhí)行環(huán)境中呈野,于是我采用npm install的方式來下載了:
我新建了一個(gè)叫AMD的文件夾,作為AMD的案例印叁。
在項(xiàng)目的根目錄下執(zhí)行:
npm i requirejs
(找了一圈NPM也沒看到能使用CDN遠(yuǎn)程引入的)
執(zhí)行完畢之后被冒,項(xiàng)目的根目錄下出現(xiàn)了依賴包,打開看了看轮蜕,確實(shí)是下載下來了昨悼。
現(xiàn)在可以開心的在項(xiàng)目里用define()了 。
來看個(gè)小例子跃洛,我重新定義了一個(gè)math.js:
math.js

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

這里模塊很簡單率触,導(dǎo)出了一個(gè)加法函數(shù)。
(至于這里為什么add: add要這樣寫汇竭,而不是只簡寫為add呢闲延?別忘了這種對象同名屬性簡寫是ES6才出來的)
3. 引用模塊
坑二:
OK,既然模塊已經(jīng)能導(dǎo)出了韩玩,那就讓我們來看看如何引用吧,依照著教材陆馁,我在test.js中引入了math模塊并想要調(diào)用add()方法:
test.js:

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

之后熟練的執(zhí)行node test.js找颓。
不出意外,又報(bào)錯(cuò)了
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
確認(rèn)了一下叮贩,和教材們中的寫法一樣啊击狮,第一個(gè)參數(shù)為要加載的模塊數(shù)組佛析,第二個(gè)參數(shù)為加載完之后的回調(diào)。
難受..原來上面??require([modules], callback)這樣的寫法它和define一樣都只是個(gè)噱頭彪蓬,如果你真得用的話寸莫,還是得用JavaScript庫中的方法。
由于上面已經(jīng)安裝過requirejs了档冬,這里我直接使用就可以了膘茎,現(xiàn)在我修改了一下test.js文件:

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

好了,現(xiàn)在執(zhí)行node test.js就可以正常使用了..
4. 依賴其它模塊的define
可以看到define它還有另外兩個(gè)參數(shù)的酷誓,第一個(gè)是模塊的名稱披坏,沒啥好說的,讓我們來看看第二個(gè)它所依賴的模塊盐数。
還記得在CommonJS規(guī)范那里我們寫了一個(gè)m1.js嗎棒拂?現(xiàn)在就讓我們把這個(gè)模塊拿來用下,把它作為math.js中的一個(gè)依賴玫氢。
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模塊內(nèi)的, 但是我不依賴math')
};
other();

所以我們可以看到漾峡,依賴關(guān)系依次為:
test -> math -> m1
如果按照AMD的規(guī)范攻旦,模塊的加載需要依靠前一個(gè)模塊加載完才會(huì)執(zhí)行回調(diào)函數(shù)內(nèi)的內(nèi)容,那么我們可以想象當(dāng)我在終端輸入node test.js的時(shí)候灰殴,要出現(xiàn)的結(jié)果應(yīng)該是:

LinDaiDaideMBP:commonJS lindaidai$ node test.js
我是test模塊內(nèi)的, 但是我不依賴math
我是m1, 我被加載了...
我是math, 我被加載了...
我是test, 我被加載了...
3
lindaidai

(這個(gè)敬特,相信大家應(yīng)該都看清了彼此的依賴關(guān)系吧)
但是現(xiàn)實(shí)總是那么的殘酷,當(dāng)我按下回車的時(shí)候牺陶,又報(bào)錯(cuò)了...
ReferenceError: module is not defined
看了一下這個(gè)報(bào)錯(cuò)的內(nèi)容伟阔,是在m1.js中...呆了幾秒鐘反應(yīng)了過來...
既然是使用AMD的規(guī)范,那我們肯定是要一統(tǒng)到底了掰伸,m1.js中用的還是CommonJS的規(guī)范皱炉,當(dāng)然不行了。
OK狮鸭,來修改一下m1.js:
m1.js:

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

(當(dāng)然據(jù)我的了解合搅,requirejs還可用于在script中引用然后定義網(wǎng)頁程序的主模塊等使用,可以看一下:
http://www.ruanyifeng.com/blog/2012/11/require_js.html
AMD的知識(shí)點(diǎn)大概就介紹到了這里歧蕉,相信大家也知道它的基本使用了吧灾部,至于其中的一些區(qū)別什么的我在最后也會(huì)列一份清單,不過現(xiàn)在讓我們先來看看CMD吧惯退。

4.CMD規(guī)范

CMD (Common Module Definition), 是seajs推崇的規(guī)范赌髓,CMD則是依賴就近,用的時(shí)候再require。

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

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

看著和AMD有點(diǎn)像的夷野,沒錯(cuò),其實(shí)define()的參數(shù)甚至都是一樣的:
define(id?, dependencies?, factory)
但是區(qū)別在于哪里呢荣倾?讓我們來看看最后一個(gè)factory它參數(shù)悯搔。
factory函數(shù)中是會(huì)接收三個(gè)參數(shù):

  • require
  • exports
  • module

這三個(gè)很好理解,對應(yīng)著之前的CommonJS那不就是:

  • require:引入某個(gè)模塊
  • exports:當(dāng)前模塊的exports舌仍,也就是module.exports的簡寫
  • module:當(dāng)前這個(gè)模塊

現(xiàn)在再來說說AMD和CMD的區(qū)別妒貌。
雖然它們的define()方法的參數(shù)都相同,但是:

  • AMD中會(huì)把當(dāng)前模塊的依賴模塊放到dependencies中加載抡笼,并在factory回調(diào)中拿到加載成功的依賴
  • CMD一般不在dependencies中加載苏揣,而是寫在factory中,使用require加載某個(gè)依賴模塊

因此才有了我們常惩埔觯看到的一句話:

AMD和CMD最大的區(qū)別是對依賴模塊的執(zhí)行時(shí)機(jī)處理不同平匈,注意不是加載的時(shí)機(jī)或者方式不同,二者皆為異步加載模塊藏古。

比較有名一點(diǎn)的增炭,seajs,來看看它推薦的CMD 模塊書寫格式吧:

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

這是官網(wǎng)的一個(gè)小案例拧晕,我也去seajs的文檔中看了一下沒啥太大問題隙姿,這里就不舉例了。

5.AMD和CMD的區(qū)別

同樣是math模塊中需要加載m1模塊厂捞。
在AMD中我們會(huì)這樣寫:
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输玷,我們會(huì)這樣寫:
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
  }
})

假如此時(shí)m1.js中有一個(gè)語句是在m1模塊被加載的時(shí)候打印出"我是m1, 我被加載了..."。
執(zhí)行結(jié)果區(qū)別:

  • AMD靡馁,會(huì)先加載m1欲鹏,"我是m1"會(huì)先執(zhí)行
  • CMD,我是"我是math"會(huì)先執(zhí)行臭墨,因?yàn)楸绢}中console.log('我是math, 我被加載了...')是放在require('m1')前面的赔嚎。

現(xiàn)在可以很明顯的看到區(qū)別了。
AMD依賴前置胧弛,js很方便的就知道要加載的是哪個(gè)模塊了尤误,因?yàn)橐呀?jīng)在define的dependencies參數(shù)中就定義好了,會(huì)立即加載它结缚。
CMD是就近依賴损晤,需要使用把模塊變?yōu)樽址馕鲆槐椴胖酪蕾嚵四切┠K。
OK红竭,來看個(gè)總結(jié):
兩者之間沉馆,最明顯的區(qū)別就是在模塊定義時(shí)對依賴的處理不同
1码党、AMD推崇依賴前置,在定義模塊的時(shí)候就要聲明其依賴的模塊2斥黑、CMD推崇就近依賴,只有在用到某個(gè)模塊的時(shí)候再去require

6.ES6 Modules規(guī)范

ES6標(biāo)準(zhǔn)出來后眉厨,ES6 Modules規(guī)范算是成為了前端的主流吧锌奴,以import引入模塊,export導(dǎo)出接口被越來越多的人使用憾股。
下面鹿蜀,我也會(huì)從這么幾個(gè)方面來介紹ES6 Modules規(guī)范:
export命令和import命令可以出現(xiàn)在模塊的任何位置,只要處于模塊頂層就可以服球。如果處于塊級(jí)作用域內(nèi)茴恰,就會(huì)報(bào)錯(cuò),這是因?yàn)樘幱跅l件代碼塊之中斩熊,就沒法做靜態(tài)優(yōu)化了往枣,違背了ES6模塊的設(shè)計(jì)初衷。
1. export導(dǎo)出模塊
export有兩種模塊導(dǎo)出方式:

  • 命名式導(dǎo)出(名稱導(dǎo)出)
  • 默認(rèn)導(dǎo)出(自定義導(dǎo)出)

命名式導(dǎo)出
來看幾種正確和錯(cuò)誤的寫法吧:

// 以下兩種為錯(cuò)誤
// 1.
export 1;
// 2.
const a = 1;
export a;
// 以下為正確
// 3.
const a = 1;
export { a };
// 4. 接口名與模塊內(nèi)部變量之間粉渠,建立了一一對應(yīng)的關(guān)系
export const a = 1, b = 2;
// 5. 接口名與模塊內(nèi)部變量之間分冈,建立了一一對應(yīng)的關(guān)系
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 };

容易混淆的可能是2和4兩種寫法了,看著很像霸株,但是2卻不行雕沉。2直接導(dǎo)出一個(gè)值為1的變量是和情況一一樣,沒有什么意義去件,因?yàn)槟阍诤竺嬉玫臅r(shí)候并不能完成解構(gòu)坡椒。
但是4中,接口名與模塊內(nèi)部變量之間尤溜,建立了一一對應(yīng)的關(guān)系倔叼,所以可以。
默認(rèn)導(dǎo)出
默認(rèn)導(dǎo)出會(huì)在export后面加上一個(gè)default:

// 1.
const a = 1;
export default a;
// 2.
const a = 1;
export default { a };
// 3.
export default function() {}; // 可以導(dǎo)出一個(gè)函數(shù)
export default class(){}; // 也可以出一個(gè)類

其實(shí)靴跛,默認(rèn)導(dǎo)出可以理解為另一種形式上的命名導(dǎo)出缀雳,也就是說a這個(gè)屬性名相當(dāng)于是被我重寫了成了default:

const a = 1;
export defalut a;
// 等價(jià)于
export { a as default }

所以,我們才可以用const a = 1; export default a;這種方式導(dǎo)出一個(gè)值梢睛。
2. import導(dǎo)入模塊
import模塊導(dǎo)入與export模塊導(dǎo)出功能相對應(yīng)肥印,也存在兩種模塊導(dǎo)入方式:命名式導(dǎo)入(名稱導(dǎo)入)和默認(rèn)導(dǎo)入(定義式導(dǎo)入)。
來看看寫法:


// 某個(gè)模塊的導(dǎo)出 moudule.js
export const a = 1;

// 模塊導(dǎo)入
// 1. 這里的a得和被加載的模塊輸出的接口名對應(yīng)
import { a } from './module'
// 2. 使用 as 換名
import { a as myA } from './module'
// 3. 若是只想要運(yùn)行被加載的模塊可以這樣寫绝葡,但是即使加載2次也只是運(yùn)行一次
import './module'
// 4. 整體加載
import * as module from './module'
// 5. default接口和具名接口
import module, { a } from './module'

第四種寫法會(huì)獲取到module中所有導(dǎo)出的東西深碱,并且賦值到module這個(gè)變量下,這樣我們就可以用module.a這種方式來引用a了藏畅。
3. export ... from...
其實(shí)還有一種寫法敷硅,可以將export和from結(jié)合起來用功咒。
例如,我有三個(gè)模塊a绞蹦、b力奋、c。
c模塊現(xiàn)在想要引入a模塊幽七,但是它不不直接引用a景殷,而是通過b模塊來引用,那么你可能會(huì)想到b應(yīng)該這樣寫:

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

引入someVariable然后再導(dǎo)出澡屡。
這還只是一個(gè)變量猿挚,我們得導(dǎo)入再導(dǎo)出,若是有很多個(gè)變量需要這樣驶鹉,那無疑會(huì)增加很多代碼量绩蜻。
所以這時(shí)候可以用下面這種方式來實(shí)現(xiàn):
export { someVariable } from './a';
不過這種方式有一點(diǎn)需要注意:
這樣的方式不會(huì)將數(shù)據(jù)添加到該聚合模塊的作用域, 也就是說, 你無法在該模塊(也就是b)中使用someVariable。
4. ES6 Modules規(guī)范的特點(diǎn)
總結(jié)一下它的特點(diǎn)哈:

  • 輸出使用export
  • 輸入使用import
  • 可以使用export...from...這種寫法來達(dá)到一個(gè)"中轉(zhuǎn)"的效果
  • 輸入的模塊變量是不可重新賦值的室埋,它只是個(gè)可讀引用办绝,不過卻可以改寫屬性
  • export命令和import命令可以出現(xiàn)在模塊的任何位置,只要處于模塊頂層就可以词顾。如果處于塊級(jí)作用域內(nèi)八秃,就會(huì)報(bào)錯(cuò),這是因?yàn)樘幱跅l件代碼塊之中肉盹,就沒法做靜態(tài)優(yōu)化了昔驱,違背了ES6模塊的設(shè)計(jì)初衷。
  • import命令具有提升效果上忍,會(huì)提升到整個(gè)模塊的頭部骤肛,首先執(zhí)行。

5. Bable下的ES6模塊轉(zhuǎn)換
還有一點(diǎn)就是窍蓝,如果你有使用過一些ES6的Babel的話腋颠,你會(huì)發(fā)現(xiàn)當(dāng)使用export/import的時(shí)候,Babel也會(huì)把它轉(zhuǎn)換為exports/require的形式吓笙。
例如我的輸出:
m1.js:
export const count = 0;
我的輸入:
index.js:

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

當(dāng)使用Babel編譯之后淑玫,各自會(huì)被轉(zhuǎn)換為:
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);

正是因?yàn)檫@種轉(zhuǎn)換關(guān)系,才能讓我們把exports和import結(jié)合起來用:
也就是說你可以這樣用:

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

7.CommonJS與ES6 Modules規(guī)范的區(qū)別

我相信很多人就比較關(guān)心它兩區(qū)別的問題面睛,因?yàn)榛旧厦嬖噯柕木褪沁@個(gè)絮蒿。好吧,這里來做一個(gè)算是比較詳細(xì)的總結(jié)吧叁鉴。

  • CommonJS模塊是運(yùn)行時(shí)加載土涝,ES6 Modules是編譯時(shí)輸出接口
  • CommonJS輸出是值的拷貝;ES6 Modules輸出的是值的引用幌墓,被輸出模塊的內(nèi)部的改變會(huì)影響引用的改變
  • CommonJs導(dǎo)入的模塊路徑可以是一個(gè)表達(dá)式但壮,因?yàn)樗褂玫氖莚equire()方法冀泻;而ES6 Modules只能是字符串
  • CommonJS this指向當(dāng)前模塊,ES6 Modules this指向undefined
  • 且ES6 Modules中沒有這些頂層變量:arguments蜡饵、require弹渔、module、exports验残、__filename捞附、__dirname

關(guān)于第一個(gè)差異,是因?yàn)镃ommonJS 加載的是一個(gè)對象(即module.exports屬性)您没,該對象只有在腳本運(yùn)行完才會(huì)生成。(commonjs輸出的是淺拷貝)而 ES6 模塊不是對象胆绊,它的對外接口只是一種靜態(tài)定義氨鹏,在代碼靜態(tài)解析階段就會(huì)生成。

8.拓展

目前所有的引擎都還沒有實(shí)現(xiàn)import压状,在node中使用babel支持ES6仆抵,也僅僅是將ES6轉(zhuǎn)碼為ES5再執(zhí)行,import語法會(huì)被轉(zhuǎn)碼為require种冬。這也是為什么在模塊導(dǎo)出時(shí)使用module.exports镣丑,在引入模塊時(shí)使用import仍然起效,因?yàn)楸举|(zhì)上娱两,import會(huì)被轉(zhuǎn)碼為require去執(zhí)行莺匠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市十兢,隨后出現(xiàn)的幾起案子趣竣,更是在濱河造成了極大的恐慌,老刑警劉巖旱物,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件遥缕,死亡現(xiàn)場離奇詭異,居然都是意外死亡宵呛,警方通過查閱死者的電腦和手機(jī)单匣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宝穗,“玉大人户秤,你說我怎么就攤上這事》碛” “怎么了虎忌?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長橱鹏。 經(jīng)常有香客問我膜蠢,道長堪藐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任挑围,我火速辦了婚禮礁竞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杉辙。我一直安慰自己模捂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布蜘矢。 她就那樣靜靜地躺著狂男,像睡著了一般。 火紅的嫁衣襯著肌膚如雪品腹。 梳的紋絲不亂的頭發(fā)上岖食,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機(jī)與錄音舞吭,去河邊找鬼泡垃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛羡鸥,可吹牛的內(nèi)容都是我干的蔑穴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼惧浴,長吁一口氣:“原來是場噩夢啊……” “哼存和!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赶舆,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤哑姚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后芜茵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叙量,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年九串,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绞佩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,673評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猪钮,死狀恐怖品山,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情烤低,我是刑警寧澤肘交,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站扑馁,受9級(jí)特大地震影響涯呻,放射性物質(zhì)發(fā)生泄漏凉驻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一复罐、第九天 我趴在偏房一處隱蔽的房頂上張望涝登。 院中可真熱鬧,春花似錦效诅、人聲如沸胀滚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咽笼。三九已至,卻和暖如春戚炫,著一層夾襖步出監(jiān)牢的瞬間褐荷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工嘹悼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人层宫。 一個(gè)月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓杨伙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親萌腿。 傳聞我的和親對象是個(gè)殘疾皇子限匣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容