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