理解前端模塊概念:CommonJs與ES6Module

前言

現(xiàn)代前端開(kāi)發(fā)每時(shí)每刻都和模塊打交道。例如梆惯,在項(xiàng)目中引入一個(gè)插件备韧,或者實(shí)現(xiàn)一個(gè)供全局使用組件的JS文件劫樟。這些都可以稱(chēng)為模塊。

在設(shè)計(jì)程序結(jié)構(gòu)時(shí)织堂,不可能把所有代碼都放在一起叠艳。更為友好的組織方式時(shí)按照特定的功能將代碼拆分為多個(gè)代碼片段,每個(gè)片段實(shí)現(xiàn)一個(gè)功能或者一個(gè)特定的目的易阳,然后通過(guò)接口的方式組合在一起附较。這就是模塊思想。

JavaScript里的模塊

眾所周知闽烙,JavaScript在早期是沒(méi)有模塊這一概念翅睛。唯有通過(guò)srcipt標(biāo)簽將多個(gè)js文件一個(gè)個(gè)的插入在HTML中。當(dāng)項(xiàng)目越來(lái)越大時(shí)黑竞,這種方式會(huì)導(dǎo)致很多問(wèn)題:

  • 需要手動(dòng)維護(hù)JavaScript的加載順序捕发。因?yàn)橥ǔcript之間會(huì)有很多依賴(lài)關(guān)系,但這種關(guān)系都是隱式的很魂,除非一個(gè)個(gè)去查看注釋?zhuān)ㄈ绻麤](méi)有注釋?zhuān)蔷?..)扎酷,否則很難指明誰(shuí)依賴(lài)誰(shuí)。

  • 命名沖突遏匆。所有script文件所定義的所有內(nèi)容都由全局作用域共享法挨。一個(gè)人開(kāi)發(fā)還好,碰上多人協(xié)作開(kāi)發(fā)幅聘,那就是災(zāi)害凡纳。

  • 如果script數(shù)量太多,這也會(huì)影響頁(yè)面加載帝蒿。因?yàn)閟cript標(biāo)簽都需要向服務(wù)器請(qǐng)求資源荐糜,過(guò)多的請(qǐng)求會(huì)嚴(yán)重降低渲染的速度。

而模塊化就能很好的解決以上問(wèn)題葛超。

何為模塊

模塊是使用不同方式加載的JS文件暴氏。這種不同模式很有必要,因?yàn)樗c腳本有著非常不同的語(yǔ)義绣张,在ES6的模塊中有著下列的語(yǔ)義:

  • 模塊自動(dòng)運(yùn)行在嚴(yán)格模式下答渔。

  • 在模塊的頂層作用域創(chuàng)建的變量,不會(huì)被自動(dòng)添加在共享的全局作用域中侥涵,它們之后在各自的模塊頂層作用域下生存沼撕。

  • 通過(guò)導(dǎo)入導(dǎo)出的語(yǔ)句宋雏,可以非常清晰的指明模塊的依賴(lài)關(guān)系。

這些差異代表了JS代碼加載與執(zhí)行方面的顯著改變端朵。

模塊的發(fā)展

從2009年開(kāi)始好芭,JavaScript社區(qū)開(kāi)始對(duì)模塊化不斷進(jìn)行嘗試,并依次出現(xiàn)了AMD冲呢、CommonJs舍败、CMD等解決方案。

Nodejs是CommonJs規(guī)范的主要實(shí)踐者敬拓,所以也是這幾個(gè)方案里面使用最廣泛的方案邻薯。但這些都只是由社區(qū)提出的規(guī)范,并不能算語(yǔ)言本身的特性乘凸。

到了2015年厕诡,ECMAScript6.0正式定義了JavaScript模塊標(biāo)準(zhǔn)。

CommonJs

JavaScript在2009年提出了CommonJs規(guī)范营勤,包含了模塊灵嫌、文件、IO葛作、控制臺(tái)在內(nèi)的一系列標(biāo)準(zhǔn)寿羞。并且nodejs實(shí)現(xiàn)了CommonJS中的一部分,并在其基礎(chǔ)上進(jìn)行了一些調(diào)整÷复溃現(xiàn)在我們所說(shuō)的CommonJs其實(shí)是Nodejs中的版本绪穆,并非它的原始定義。

CommonJs最初只為服務(wù)器端服務(wù)的虱岂。因?yàn)樗x的兩個(gè)主要概念:

  • require函數(shù)玖院,用于導(dǎo)入;

  • module.exports變量第岖,用于導(dǎo)出难菌;

這兩個(gè)關(guān)鍵字,瀏覽器都不支持蔑滓。直到社區(qū)出現(xiàn)了諸如browserify編譯庫(kù)將commonjs編譯成為瀏覽器所能支持的語(yǔ)法郊酒。這就意味著客戶(hù)端代碼可以遵循Commonjs標(biāo)準(zhǔn)來(lái)編寫(xiě)了。

不僅如此烫饼,借助Nodejs的npm包管理器,開(kāi)發(fā)人員還可以獲取社區(qū)上的優(yōu)秀代碼庫(kù)试读,或者將自己的代碼發(fā)布出去以供需要的人使用杠纵。這種方式使CommonJs在前端開(kāi)發(fā)中逐漸流行了起來(lái)。

模塊

CommonJs規(guī)范中規(guī)定了每一個(gè)文件都是一個(gè)模塊钩骇。使用require導(dǎo)入的文件會(huì)形成一個(gè)屬于自身的模塊作用域比藻,這樣就不會(huì)在進(jìn)行變量以及函數(shù)聲明時(shí)會(huì)污染全局作用域铝量。所有的變量和函數(shù)都只有模塊自身能訪問(wèn),對(duì)外不可見(jiàn)的银亲。

舉例:

// foo.js
var name = 'foo';

// bar.js
var name = 'bar';
require('./foo.js');
console.log(name); // bar

在bar.js中通過(guò)require函數(shù)加載foo.js慢叨。運(yùn)行之后輸出的結(jié)果是‘bar’,這就說(shuō)明了foo.js中的變量聲明并不會(huì)影響bar.js务蝠。每個(gè)文件都擁有自己的作用域拍谐。

導(dǎo)出

導(dǎo)出是一個(gè)模塊對(duì)外暴露自身的唯一方式。在CommonJs中馏段,通過(guò)module.exports可以導(dǎo)出模塊中的內(nèi)容轩拨。

舉例:

module.exports = {
    name: 'foo'
}

CommonJs模塊內(nèi)部會(huì)有一個(gè)module對(duì)象用于存放當(dāng)前模塊的信息,可以理解為在每個(gè)模塊的最開(kāi)始中定義了以下對(duì)象:

var module = {};
// ...
module.exports = {};

CommonJs也支持另一種導(dǎo)出方式:exports院喜。

exports.name = 'foo'

在實(shí)現(xiàn)上亡蓉,這段代碼與上面的module.exports沒(méi)有不同,其內(nèi)在機(jī)制是將exports指向了module.exports喷舀】潮簦可以簡(jiǎn)單的理解為CommonJs在每個(gè)模塊的開(kāi)頭默認(rèn)添加了以下代碼:

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

因此,為export.name賦值相當(dāng)于在module.exports對(duì)象上添加了一個(gè)name屬性硫麻。

也很容易看出exports與module.exports只是指向同一個(gè)對(duì)象爸邢。所以對(duì)exports進(jìn)行賦值操作,使其指向新的對(duì)象庶香,就會(huì)失效了甲棍。

舉例:

exports = {
    name: 'foo'
}

此時(shí)name屬性并不會(huì)被導(dǎo)出。

另外這兩個(gè)方法赶掖,并不能一起運(yùn)用感猛。因?yàn)槭褂胢odule.exports賦值就相當(dāng)于使其指向新的對(duì)象。之前的exports賦值都會(huì)失效奢赂。

導(dǎo)入

CommonJs使用require函數(shù)進(jìn)行導(dǎo)入操作陪白。

舉例:

// foo.js
module.exports = {
    sayname: function () {
        console.log('foo');
    }
};

// bar.js
var sayname = require('./foo.js').sayname;
sayname(); // foo

在bar.js中導(dǎo)入了foo.js,并調(diào)用了它的sayname函數(shù)膳灶。

當(dāng)require一個(gè)模塊時(shí)會(huì)有兩種情況:

  • 模塊是第一次被require加載咱士。這時(shí)會(huì)首先執(zhí)行該模塊,然后導(dǎo)出內(nèi)容轧钓。

  • 模塊是曾經(jīng)被require加載過(guò)序厉。這時(shí)會(huì)直接導(dǎo)出執(zhí)行得到的結(jié)果。

舉例:

// foo.js
console.log('running foo.js')
exports.name = 'foo';

// bar.js
var firstname = require('./foo').name;
console.log('firstname:', firstname); 

var lastname = require('./foo').name;
console.log('lastname:', lastname);

輸出的是:

running foo.js
fistname:foo

lastname:foo

從上面代碼看有兩個(gè)地方都require了foo文件毕箍,但從結(jié)果看弛房,只運(yùn)行了一遍foo.js。

module對(duì)象中有一個(gè)loaded屬性用于記錄該模塊是否被加載過(guò)而柑。默認(rèn)值為false文捶,當(dāng)模塊第一次被加載時(shí)荷逞,會(huì)賦值為true,后面再次加載時(shí)會(huì)檢查module.loaded是否為true粹排,如果是种远,則直接返回結(jié)果,并不會(huì)再次執(zhí)行代碼顽耳。

require函數(shù)可以接受表達(dá)式坠敷,借助這個(gè)特性可以動(dòng)態(tài)地制定模塊的加載路徑。

舉例:

var path = ['foo.js', 'bar.js'];
path.forEach(name => {
    require('./' + name);
})

ES6Module

CommonJs可以說(shuō)是比較好的解決了模塊的問(wèn)題斧抱,但這些都只是由社區(qū)提出的規(guī)范常拓,并不能算語(yǔ)言本身的特性。

到了2015年辉浦,ECMAScript6.0正式定義了JavaScript模塊標(biāo)準(zhǔn)弄抬。從此 JavaScript 語(yǔ)言才具備了模塊這一特性。

模塊

將前面CommonJs的例子宪郊,用ES6Module方式改寫(xiě)掂恕。

// foo.js
export default {
    sayname: function () {
        console.log('foo');
    }
};

// bar.js
import foo from './foo.js'
foo.sayname(); // foo

ES6Module也是將每個(gè)文件作為一個(gè)模塊,每個(gè)模塊擁有自身的作用域弛槐,不同的是導(dǎo)入懊亡、導(dǎo)出語(yǔ)句。import和export也是作為保留關(guān)鍵字在ES6版本加入了進(jìn)來(lái)乎串,而且ES6Module會(huì)自動(dòng)采用嚴(yán)格模式店枣。

導(dǎo)出

在ES6Module中使用export命令來(lái)導(dǎo)出模塊。export有兩種形式:

  • 命名導(dǎo)出

  • 默認(rèn)導(dǎo)出

1叹誉、命令導(dǎo)出 一個(gè)模塊可以有多個(gè)命名導(dǎo)出鸯两,有兩種不同的寫(xiě)法:

// 1
export const name = 'foo';

// 2
const name = 'foo';
export { name }

可以通過(guò)as關(guān)鍵字對(duì)變量重命名。

例如:

const name = 'foo';
export { name as nickname }

2长豁、默認(rèn)導(dǎo)出

默認(rèn)導(dǎo)出只能有一個(gè):

export default {
    name: 'foo'
}

可以將export default理解為對(duì)外輸出一個(gè)名為default的變量钧唐。

導(dǎo)入

ES6Module使用import語(yǔ)法導(dǎo)入模塊。

舉例:

// foo.js
export const name = 'foo';
// bar.js
import { name } from './foo'
console.log(name)

加載帶有命令導(dǎo)出的模塊時(shí)匠襟,import后面要跟一對(duì)大括號(hào)來(lái)將導(dǎo)入的變量名包裹起來(lái)钝侠,并且這寫(xiě)變量名需要與導(dǎo)出的變量名完全一致。導(dǎo)入變量的效果相當(dāng)于在當(dāng)前作用域下聲明了這些變量酸舍,并且不可以對(duì)齊進(jìn)行修改帅韧。

與命令導(dǎo)出類(lèi)似,也可以通過(guò)as關(guān)鍵字對(duì)導(dǎo)入的變量重命名啃勉。

舉例:

// foo.js
export const name = 'foo';
// bar.js
import { name as nickname } from './foo'
console.log(nickname)

// 也可以通過(guò)整體導(dǎo)入的方法
import * as name from './foo'
console.log(name.name)

默認(rèn)導(dǎo)入的例子:

// foo.js
export default {
    name: 'foo'
}

// bar
import name from './foo'
console.log(name.name)

CommonJs與ES6Module的區(qū)別

對(duì)模塊依賴(lài)的處理區(qū)別

CommonJs與ES6Module最本質(zhì)的區(qū)別在于前者對(duì)模塊依賴(lài)的解決是動(dòng)態(tài)的忽舟,而后者是靜態(tài)的。

  • 動(dòng)態(tài):模塊依賴(lài)關(guān)系的建立是發(fā)生在代碼運(yùn)行階段;

  • 靜態(tài):模塊依賴(lài)關(guān)系的建立是發(fā)生在代碼編譯階段萧诫;

在CommonJs中,當(dāng)模塊A加載模塊B時(shí)枝嘶,會(huì)執(zhí)行B的代碼帘饶,將其module.exports對(duì)象作為require函數(shù)的返回值進(jìn)行返回。并且requrie的模塊路徑可以動(dòng)態(tài)指定群扶,支持傳入一個(gè)表示式及刻,甚至可以使用if語(yǔ)句判斷是否加載某個(gè)模塊。所以CommonJs模塊被執(zhí)行前竞阐,并沒(méi)有辦法確定明確的依賴(lài)關(guān)系缴饭,模塊的導(dǎo)入,導(dǎo)出發(fā)生在代碼的運(yùn)行階段骆莹。

ES6Module的導(dǎo)入颗搂、導(dǎo)出語(yǔ)句都是聲明式的,不支持導(dǎo)入的路徑是一個(gè)表達(dá)式幕垦,并且導(dǎo)入丢氢、導(dǎo)出語(yǔ)句必須位于模塊的頂層作用域。在ES6代碼的編譯階段就可以分析出模塊的依賴(lài)關(guān)系先改。

導(dǎo)入模塊值的區(qū)別

在導(dǎo)入一個(gè)模塊時(shí)疚察,對(duì)于CommonJs來(lái)說(shuō)是得到了一個(gè)導(dǎo)出值的拷貝;而在ES6Module中則是值的動(dòng)態(tài)映射仇奶,并且這個(gè)映射是只讀的貌嫡。

舉例:

// foo
var count = 0;
module.exports = {
    count: count,
    add: function (a, b) {
        count++;
        return a+b;
    }
}

// bar
var count = require('./foo').count;
var add = require('./foo').add;

console.log(count); // 0
add(2,3);
console.log(count); // 0

count += 1;
console.log(count); // 1(拷貝值可以更改)

bar中的count是對(duì)foo中count的一份值拷貝,因此在調(diào)用add函數(shù)時(shí)该溯,雖然更改了foo中count的值岛抄,但是并不會(huì)對(duì)bar中導(dǎo)入值造成更改。

另一方面拷貝值可以進(jìn)行更改朗伶。

使用ES6Module進(jìn)行改寫(xiě)

// foo
let count = 0;
const add = function (a,b) {
    count++;
    return a+b;
}
export { count, add }

// bar
import { count, add } from './foo';
console.log(count); // 0
add(2,3);
console.log(count); // 1(實(shí)時(shí)反映foo中count的值)

count++; // 報(bào)錯(cuò) count is read-only

可以將映射關(guān)系理解為一面鏡子弦撩,從鏡子中可以實(shí)時(shí)觀察到原有的事物,但不能操作鏡子中的影像论皆。

循環(huán)依賴(lài)的區(qū)別

CommonJs中循環(huán)依賴(lài)的例子:

// foo
const bar = require('./bar')
console.log('來(lái)自bar:', bar);
module.exports = 'foo';

// bar
const foo = require('./foo');
console.log('來(lái)自foo:', foo);
module.exports = 'bar;

// index
require('./foo')

在控制臺(tái)輸出:

來(lái)自foo:()
來(lái)自bar:bar

首先來(lái)梳理執(zhí)行流程:

  • index文件中引入了foo益楼,此時(shí)開(kāi)始執(zhí)行foo中的代碼;

  • foo第一句導(dǎo)入了bar点晴,這是foo不會(huì)繼續(xù)向下執(zhí)行感凤,而是進(jìn)入了bar的內(nèi)部。

  • 在bar中又引入了foo粒督,這里產(chǎn)生了循環(huán)依賴(lài)陪竿。但并不會(huì)再次執(zhí)行foo,而是直接導(dǎo)出返回值,也就是module.exports族跛。但由于foo未執(zhí)行完闰挡,導(dǎo)出值是默認(rèn)的空對(duì)象,因此當(dāng)bar執(zhí)行到console.log時(shí)礁哄,打印出來(lái)的是空對(duì)象长酗。

  • bar執(zhí)行完畢,foo繼續(xù)向下執(zhí)行直到流程結(jié)束桐绒。

使用ES6Module重寫(xiě)上面例子:

// foo
import bar from './bar';
console.log('來(lái)自bar:', bar);
export default 'foo'

// bar
import foo from './foo'
console.log('來(lái)自foo:', foo);
export default 'bar'

// index
import foo from './foo'

結(jié)果是: 來(lái)自foo: undefined 來(lái)自bar:bar

在bar中同樣無(wú)法得到foo正確的導(dǎo)出值夺脾,只不過(guò)和CommonJS默認(rèn)導(dǎo)出一個(gè)空對(duì)象不同,這里獲取到的是undefined茉继。

結(jié)尾

模塊是程序設(shè)計(jì)的重要概念咧叭,希望上述內(nèi)容能讓你了解到前端模塊的概念。詳細(xì)的用法搜索官網(wǎng)或者書(shū)籍進(jìn)行學(xué)習(xí)烁竭。書(shū)籍推薦

作者:zhangwinwin
鏈接:理解前端模塊概念:CommonJs與ES6Module
來(lái)源:github

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末菲茬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子派撕,更是在濱河造成了極大的恐慌生均,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腥刹,死亡現(xiàn)場(chǎng)離奇詭異马胧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)衔峰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)佩脊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人垫卤,你說(shuō)我怎么就攤上這事威彰。” “怎么了穴肘?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵歇盼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我评抚,道長(zhǎng)豹缀,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任慨代,我火速辦了婚禮邢笙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘侍匙。我一直安慰自己氮惯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著妇汗,像睡著了一般帘不。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杨箭,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天厌均,我揣著相機(jī)與錄音,去河邊找鬼告唆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晶密,可吹牛的內(nèi)容都是我干的擒悬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼稻艰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼懂牧!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起尊勿,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤僧凤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后元扔,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體躯保,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年澎语,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了途事。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡擅羞,死狀恐怖尸变,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情减俏,我是刑警寧澤召烂,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站娃承,受9級(jí)特大地震影響奏夫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜历筝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一桶蛔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧漫谷,春花似錦仔雷、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)电抚。三九已至,卻和暖如春竖共,著一層夾襖步出監(jiān)牢的瞬間蝙叛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工公给, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留借帘,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓淌铐,卻偏偏與公主長(zhǎng)得像肺然,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子腿准,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355