前言
現(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