本文是深入理解JavaScript系列的第三篇讀文筆記,博客原文在這里。
本文是整個(gè)深入理解JavaScript系列中第一篇稍微有點(diǎn)難啃的文章,特別是對(duì)新手來說。大叔這篇文章的寫作借鑒了下面兩篇文章,
主要參考的是前者。闡述的內(nèi)容就是模塊化模式。
首先,什么叫模塊化模式?它具有哪些特點(diǎn)?
模塊化模式(或者說模塊化編程)在JavaScript中是一個(gè)非常常見的編程技巧,它按照功能(或者業(yè)務(wù))將JavaScript代碼封裝在一起組成一個(gè)模塊,對(duì)外暴露特定的額方法和屬性。一般來說,模塊化模式具有如下幾個(gè)特點(diǎn),
文章的主體將分別介紹模塊化模式的基本用法和高級(jí)用法。
話不多說,老夫先把大叔的一段代碼拉出來溜溜,
var Calculator = function (eq) {
/*
這里可以聲明私有成員
*/
var eqCtl = document.getElementById(eq);
// 暴露公開的成員
return {
add: function (x, y) {
var val = x + y;
eqCtl.innerHTML = val;
}
};
};
我們可以通過如下的方式來調(diào)用:
var calculator = new Calculator('eq');
calculator.add(2, 2);
這里使用了new Calculator()
把函數(shù)Calculator
當(dāng)作一個(gè)構(gòu)造函數(shù)調(diào)用。有的同學(xué)可能會(huì)問,這里不使用new Calculator()
,直接調(diào)用Calculator()
函數(shù)好像也是可以的啊,那new Calculator()
和不使用new
到底啥區(qū)別?。?/p>
首先我在之前的文章中有闡述過new xxx()
這個(gè)操作的本質(zhì),不太清楚的可以移步看看。
在大叔舉的這個(gè)例子中,使用new
與不使用new
都是沒有錯(cuò)的,下面的calculator.add
方法都是可以調(diào)用的。
不過這兩者有自己適合的場(chǎng)景,
new
一定會(huì)返回一個(gè)object,其內(nèi)部的this
指向object自身,且object中方法或者屬性可以通過this
相互訪問。這種是使用JavaScript構(gòu)建OOP編程的基礎(chǔ)。new
的含義就是簡(jiǎn)單的調(diào)用函數(shù),其返回值根據(jù)函數(shù)的return
語句來確定,且內(nèi)部的this
都指向全局作用域。這種一般用于命名空間的管理,獨(dú)立模塊的封裝。閉包是函數(shù)式編程語言具有的一種語法糖(暫且這么說吧,雖然不太準(zhǔn)備)。JavaScript中對(duì)閉包的應(yīng)用隨處可見,匿名閉包更是讓一切成為可能的基礎(chǔ),而這也是JavaScript最靈活的特性。
下面我們來創(chuàng)建一個(gè)最簡(jiǎn)單的閉包函數(shù),函數(shù)內(nèi)部的代碼一直存在于閉包內(nèi),在整個(gè)運(yùn)行周期內(nèi),該閉包都保證了內(nèi)部的代碼處于私有狀態(tài)。
(function () {
// ...所有的變量和function都在這里聲明,并且作用域也只能在這個(gè)匿名閉包里
// ...但是這里的代碼依然可以訪問外部全局的對(duì)象
})();
可以看出匿名閉包其實(shí)就是一個(gè)自執(zhí)行函數(shù)。關(guān)于自執(zhí)行函數(shù)在后面的文章中還會(huì)更加深入的討論。現(xiàn)在只需要知道自執(zhí)行函數(shù)的本質(zhì)是一個(gè)函數(shù)表達(dá)式,在運(yùn)行時(shí)將會(huì)產(chǎn)生一個(gè)封閉作用域(或者說私有作用域),這個(gè)封閉作用域可以訪問外部的全局變量,但是不能被外部訪問。
這里可能會(huì)遇到一個(gè)問題,比如下面的代碼,
var name = 'a';
(function() {
console.log(name); // a
name = 'b'; // 這里不帶var的賦值,將會(huì)將name隱式的提升為全局變量,且覆蓋了閉包外部的name
console.log(name); // b
})();
console.log(name); // b
可見,由于JavaScript中存在隱式全局變量這樣一種東西,當(dāng)在閉包中需要訪問外部的全局變量時(shí),萬一操作不當(dāng),就會(huì)隱式的聲明一個(gè)你不知道的全局變量。
現(xiàn)在流行的JavaScript庫(kù),比如JQuery,都采用這樣一種方式來達(dá)到從閉包內(nèi)部訪問外部的全局變量的目的,
(function (window, undefined) {
// JQuery的源碼其實(shí)就是一個(gè)大閉包?。?})(window);
可見,我們可以將全局變量當(dāng)成一個(gè)參數(shù)傳入到匿名函數(shù)然后使用,相比隱式全局變量,它又清晰又快。
有時(shí)候可能不僅僅要使用全局變量,而是也想聲明全局變量,如何做呢?我們可以通過將匿名函數(shù)的返回值賦值給這個(gè)全局變量,代碼如下,
上面的代碼聲明了一個(gè)全局變量blogModule
,并且?guī)в?個(gè)可訪問的屬性:blogModule.AddTopic
和blogModule.Name
,除此之外,其它代碼都在匿名函數(shù)的閉包里保持著私有狀態(tài)。
下面將會(huì)闡述幾種稍微高級(jí)一點(diǎn)的用法,其實(shí)都是對(duì)上面所說的基本用法的擴(kuò)展。
前面的基本用法可能不太適合大型的項(xiàng)目。因?yàn)榇笮偷捻?xiàng)目往往會(huì)有多人協(xié)作開發(fā),每個(gè)人負(fù)責(zé)的模塊不盡相同,這時(shí)候就需要把一個(gè)模塊分割到不同的文件中去。
看下面的代碼,
var blogModule = (function(my) {
my.AddPhoto = function () {
//添加內(nèi)部代碼
};
return my;
})(blogModule);
發(fā)現(xiàn)了沒有,我們將blogModule
自身作為參數(shù)傳遞給自執(zhí)行函數(shù)。這個(gè)自執(zhí)行函數(shù)執(zhí)行完畢后,blogModule
上就會(huì)多處一個(gè)方法AddPhoto
。試想一下,多人開發(fā)中每個(gè)人都采用這種模式,那么完全就可以彼此獨(dú)立的給blogModule
添加自己所需的方法和屬性。
一般來說,我們還會(huì)做一些異常判斷,因?yàn)槎嗳藚f(xié)作的過程中,每個(gè)人都不知道自己拿到的blogModule
究竟有哪些東西,甚至都不知道這個(gè)對(duì)象是否為undefined
的,改進(jìn)如下,
var blogModule = (function(my) {
my.AddPhoto = function () {
//添加內(nèi)部代碼
};
return my;
})(blogModule || {}); // 這里是重點(diǎn)
如上面的代碼,我們?cè)趥鬟f參數(shù)的時(shí)候,其實(shí)傳入的是一個(gè)判斷表達(dá)式,這樣就保證了在閉包內(nèi)部blogModule
必定是不為undefined
,是不是很巧妙? :)
然而,有的時(shí)候我們?cè)跀U(kuò)展的時(shí)候需要對(duì)某一些方法進(jìn)行重寫,那么該怎么辦呢?看如下的代碼,
var blogModule = (function (my) {
var oldAddPhotoMethod = my.AddPhoto;
my.AddPhoto = function () {
// 重寫方法,依然可通過oldAddPhotoMethod調(diào)用舊的方法
};
return my;
})(blogModule);
代碼中,我們使用私有變量oldAddPhotoMethod
緩存了原先的AddPhoto
方法,然后重寫了AddPhoto
方法。通過這種方式,我們達(dá)到了重寫的目的,當(dāng)然如果你想在繼續(xù)在內(nèi)部使用原有的屬性,你可以調(diào)用oldAddPhotoMethod
來用。
var blogModule = (function (old) {
var my = {},
key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var oldAddPhotoMethod = old.AddPhoto;
my.AddPhoto = function () {
// 克隆以后,進(jìn)行了重寫,當(dāng)然也可以繼續(xù)調(diào)用oldAddPhotoMethod
};
return my;
})(blogModule);
說實(shí)話,我有點(diǎn)沒太看懂這部分的內(nèi)容,代碼中的for in
循環(huán)其實(shí)是將old
對(duì)象克隆了一份賦值給my
對(duì)象。但是這個(gè)克隆過程中,old
對(duì)象中的object
以及function
類型的數(shù)據(jù)其實(shí)并沒有發(fā)生克隆,只是多了個(gè)my
對(duì)象的相應(yīng)引用而已。
我不太明白的是,這個(gè)跟JavaScript的模塊化有什么關(guān)系呢?望大神解惑。
說實(shí)話,這部分的我看了很久才弄明白,別看代碼量就10來行,但是真的不是那么好理解的。(也可能是我比較菜。)
這里的需求是這樣的,一個(gè)module分割到多個(gè)文件中,我們想要各個(gè)文件中的私有變量能夠交叉訪問,該怎么做呢?
var blogModule = (function (my) {
var _private = my._private = my._private || {},
_seal = my._seal = my._seal || function () {
delete my._private;
delete my._seal;
delete my._unseal;
},
_unseal = my._unseal = my._unseal || function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};
return my;
})(blogModule || {});
任何文件都可以對(duì)他們的局部變量_private
設(shè)置,并且此設(shè)置對(duì)其他的文件也立即生效。一旦這個(gè)模塊加載結(jié)束,應(yīng)用會(huì)調(diào)用blogModule._seal()
進(jìn)行“上鎖”,這會(huì)阻止外部接入內(nèi)部的_private
。如果這個(gè)模塊需要再次增生,應(yīng)用的生命周期內(nèi),任何文件都可以調(diào)用_unseal()
進(jìn)行“開鎖”,然后再加載新文件,加載新文件后又會(huì)初始化_pravite
、_seal
和_unseal
,然后再次調(diào)用_seal()
“上鎖”。
這個(gè)過程很巧妙。其實(shí)我覺得,各個(gè)文件中的私有變量能夠交叉訪問這個(gè)需求簡(jiǎn)直就是個(gè)奇葩,為什么各個(gè)文件中的私有變量要交叉訪問呢?這不是破壞了模塊的獨(dú)立性么?有人說其實(shí)這里的多文件其實(shí)仍然描述的是同一個(gè)模塊,那我想說,這種情況下使用子模塊應(yīng)該是一種更優(yōu)的選擇。
子模塊是最簡(jiǎn)單也是最經(jīng)常使用的設(shè)計(jì)思路,
blogModule.subModule = (function () {
var my = {};
// ...
return my;
})();
其實(shí)我個(gè)人覺得,子模塊+模塊擴(kuò)展就足夠應(yīng)付一般的模塊化模式需求了。使用過多的高級(jí)用法,必定給代碼增加復(fù)雜度,給日后的維護(hù)和升級(jí)肯定為增加難度。
就我目前的經(jīng)驗(yàn)來說,模塊化模式經(jīng)常用到的幾種方法包括,松耦合擴(kuò)展,私有作用域以及子模塊。這幾種就是最常用的方式。
不過現(xiàn)在業(yè)界出現(xiàn)了CMD、AMD等規(guī)范后,JavaScript代碼的模塊化管理更趨向于代碼級(jí)別而不再是設(shè)計(jì)級(jí)別。所以本文中所描述的模塊化模式,我猜測(cè)日后將會(huì)越來越淡化,越來越輕量化,比如用在配置中,工具方法的管理上等等。
更多建議: