JavaScript的模塊化:封裝(閉包),繼承(原型) 介紹
雖然 JavaScript 天生就是一副隨隨便便的樣子,但是隨著瀏覽器能夠完成的事情越來越多,這門語言也也越來越經(jīng)常地擺出正襟危坐的架勢。在復(fù)雜的邏輯下, JavaScript 需要被模塊化,模塊需要封裝起來,只留下供外界調(diào)用的接口。閉包是 JavaScript 中實現(xiàn)模塊封裝的關(guān)鍵,也是很多初學(xué)者難以理解的要點。最初,我也陷入迷惑之中?,F(xiàn)在,我自信對這個概念已經(jīng)有了比較深入的理解。為了便于理解,文中試圖封裝一個比較簡單的對象。
我們試圖在頁面上維護(hù)一個計數(shù)器對象 ticker ,這個對象維護(hù)一個數(shù)值 n 。隨著用戶的操作,我們可以增加一次計數(shù)(將數(shù)值 n 加上 1 ),但不能減少 n 或直接改變 n 。而且,我們需要時不時查詢這個數(shù)值。
門戶大開的 JSON 風(fēng)格模塊化
一種門戶大開的方式是:
var ticker = {
n:0,
tick:function(){
this.n++;
},
};
這種方式書寫自然,而且確實有效,我們需要增加一次計數(shù)時,就調(diào)用 ticker.tick() 方法,需要查詢次數(shù)時,就訪問 ticker.n 變量。但是其缺點也是顯而易見的:模塊的使用者被允許自由地改變 n ,比如調(diào)用 ticker.n-- 或者 ticker.n=-1 。我們并沒有對 ticker 進(jìn)行封裝, n 和 tick() 看上去是 ticker 的“成員”,但是它們的可訪問性和 ticker 一樣,都是全局性的(如果 ticker 是全局變量的話)。在封裝性上,這種模塊化的方式比下面這種更加可笑的方式,只好那么一點點(雖然對有些簡單的應(yīng)用來說,這一點點也足夠了)。
var ticker = {};
var tickerN = 0;
var tickerTick = function(){
tickerN++;
}
tickerTick();
值得注意的是,在 tick() 中,我訪問的是 this.n ——這并不是因為 n 是 ticker 的成員,而是因為調(diào)用 tick() 的是 ticker 。事實上這里寫成 ticker.n 會更好,因為如果調(diào)用 tick() 的不是 ticker ,而是其他什么東西,比如:
var func = ticker.tick;
func();
這時,調(diào)用 tick() 的其實是 window ,而函數(shù)執(zhí)行時會試圖訪問 window.n 而出錯。
事實上,這種“門戶大開”型的模塊化方式,往往用來組織 JSON 風(fēng)格的數(shù)據(jù),而不是程序。比如,我們可以將下面這個 JSON 對象傳給 ticker 的某個函數(shù),來確定 ticker 從 100 開始計數(shù),每次遞進(jìn) 2 。
var config = {
nStart:100,
step:2
}
作用域鏈和閉包
來看下面的代碼,注意我們已經(jīng)實現(xiàn)了傳入 config 對 ticker 進(jìn)行自定義。
function ticker(config){
var n = config.nStart;
function tick(){
n += config.step;
}
}
console.log(ticker.n); // ->undefined
你也許會疑惑,怎么 ticker 從對象變成了函數(shù)了?這是因為 JavaScript 中只有函數(shù)具有作用域,從函數(shù)體外無法訪問函數(shù)內(nèi)部的變量。 ticker() 外訪問 ticker.n 獲得 undefined ,而 tick() 內(nèi)訪問 n 卻沒有問題。從 tick() 到 ticker() 再到全局,這就是 JavaScript 中的“作用域鏈”。
可是還有問題,那就是——怎么調(diào)用 tick() ? ticker() 的作用域?qū)?tick() 也掩蓋了起來。解決方法有兩種:
•1)將需要調(diào)用方法作為返回值,正如我們將遞增 n 的方法作為 ticker() 的返回值;
•2)設(shè)定外層作用域的變量,正如我們在 ticker() 中設(shè)置 getN 。
var getN;
function ticker(config){
var n = config.nStart;
getN = function(){
return n;
};
return function(){
n += config.step;
};
}
var tick = ticker({nStart:100,step:2});
tick();
console.log(getN()); // ->102
請看,這時,變量 n 就處在“閉包”之中,在 ticker() 外部無法直接訪問它,但是卻可以通過兩個方法來觀察或操縱它。
在本節(jié)第一段代碼中, ticker() 方法執(zhí)行之后, n 和 tick() 就被銷毀了,直到下一次調(diào)用該函數(shù)時再創(chuàng)建;但是在第二段代碼中, ticker() 執(zhí)行之后, n 不會被銷毀,因為 tick() 和 getN() 可能訪問它或改變它,瀏覽器會負(fù)責(zé)維持n。我對“閉包”的理解就是:用以保證 n 這種處在函數(shù)作用域內(nèi),函數(shù)執(zhí)行結(jié)束后仍需維持,可能被通過其他方式訪問的變量 不被銷毀的機制。
可是,我還是覺得不大對勁?如果我需要維持兩個具有相同功能的對象 ticker1 和 ticker2 ,那該怎么辦? ticker() 只有一個,總不能再寫一遍吧?
new 運算符與構(gòu)造函數(shù)
如果通過 new 運算符調(diào)用一個函數(shù),就會創(chuàng)建一個新的對象,并使用該對象調(diào)用這個函數(shù)。在我的理解中,下面的代碼中 t1 和 t2 的構(gòu)造過程是一樣的。
function myClass(){}
var t1 = new myClass();
var t2 = {};
t2.func = myClass;
t2.func();
t2.func = undefined;
t1 和 t2 都是新構(gòu)造的對象, myClass() 就是構(gòu)造函數(shù)了。類似的, ticker() 可以重新寫成。
function TICKER(config){
var n = config.nStart;
this.getN = function(){
return n;
};
this.tick = function(){
n += config.step;
}
}
var ticker1 = new TICKER({nStart:100,step:2});
ticker1.tick();
console.log(ticker1.getN()); // ->102
var ticker2 = new TICKER({nStart:20,step:3});
ticker2.tick();
ticker2.tick();
console.log(ticker2.getN()); // ->26
習(xí)慣上,構(gòu)造函數(shù)采用大寫。注意, TICKER() 仍然是個函數(shù),而不是個純粹的對象(之所以說“純粹”,是因為函數(shù)實際上也是對象, TICKER() 是函數(shù)對象),閉包依舊有效,我們無法訪問 ticker1.n 。
原型 prototype 與繼承
上面這個 TICKER() 還是有缺陷,那就是, ticker1.tick() 和 ticker2.tick() 是互相獨立的!請看,每使用 new 運算符調(diào)用 TICKER() ,就會生成一個新的對象并生成一個新的函數(shù)綁定在這個新的對象上,每構(gòu)造一個新的對象,瀏覽器就要開辟一塊空間,存儲 tick() 本身和 tick() 中的變量,這不是我們所期望的。我們期望 ticker1.tick 和 ticker2.tick 指向同一個函數(shù)對象。
這就需要引入原型。
JavaScript 中,除了 Object 對象,其他對象都有一個 prototype 屬性,這個屬性指向另一個對象。這“另一個對象”依舊有其原型對象,并形成原型鏈,最終指向 Object 對象。在某個對象上調(diào)用某方法時,如果發(fā)現(xiàn)這個對象沒有指定的方法,那就在原型鏈上一次查找這個方法,直到 Object 對象。
函數(shù)也是對象,因此函數(shù)也有原型對象。當(dāng)一個函數(shù)被聲明出來時(也就是當(dāng)函數(shù)對象被定義出來時),就會生成一個新的對象,這個對象的 prototype 屬性指向 Object 對象,而且這個對象的 constructor 屬性指向函數(shù)對象。
通過構(gòu)造函數(shù)構(gòu)造出的新對象,其原型指向構(gòu)造函數(shù)的原型對象。所以我們可以在構(gòu)造函數(shù)的原型對象上添加函數(shù),這些函數(shù)就不是依賴于 ticker1 或 ticker2 ,而是依賴于 TICKER 了。
你也許會這樣做:
function TICKER(config){
var n = config.nStart;
}
TICKER.prototype.getN = function{
// attention : invalid implementation
return n;
};
TICKER.prototype.tick = function{
// attention : invalid implementation
n += config.step;
};
請注意,這是無效的實現(xiàn)。因為原型對象的方法不能訪問閉包中的內(nèi)容,也就是變量 n 。 TICK() 方法運行之后無法再訪問到 n ,瀏覽器會將 n 銷毀。為了訪問閉包中的內(nèi)容,對象必須有一些簡潔的依賴于實例的方法,來訪問閉包中的內(nèi)容,然后在其 prototype 上定義復(fù)雜的公有方法來實現(xiàn)邏輯。實際上,例子中的 tick() 方法就已經(jīng)足夠簡潔了,我們還是把它放回到 TICKER 中吧。下面實現(xiàn)一個復(fù)雜些的方法 tickTimes() ,它將允許調(diào)用者指定調(diào)用 tick() 的次數(shù)。
function TICKER(config){
var n = config.nStart;
this.getN = function(){
return n;
};
this.tick = function(){
n += config.step;
};
}
TICKER.prototype.tickTimes = function(n){
while(n>0){
this.tick();
n--;
}
};
var ticker1 = new TICKER({nStart:100,step:2});
ticker1.tick();
console.log(ticker1.getN()); // ->102
var ticker2 = new TICKER({nStart:20,step:3});
ticker2.tickTimes(2);
console.log(ticker2.getN()); // ->26
這個 TICKER 就很好了。它封裝了 n ,從對象外部無法直接改變它,而復(fù)雜的函數(shù) tickTimes() 被定義在原型上,這個函數(shù)通過調(diào)用實例的小函數(shù)來操作對象中的數(shù)據(jù)。
所以,為了維持對象的封裝性,我的建議是,將對數(shù)據(jù)的操作解耦為盡可能小的單元函數(shù),在構(gòu)造函數(shù)中定義為依賴于實例的(很多地方也稱之為“私有”的),而將復(fù)雜的邏輯實現(xiàn)在原型上(即“公有”的)。
最后再說一些關(guān)于繼承的話。實際上,當(dāng)我們在原型上定義函數(shù)時,我們就已經(jīng)用到了繼承! JavaScript 中的繼承比 C++ 中的更……呃……簡單,或者說簡陋。在 C++ 中,我們可能會定義一個 animal 類表示動物,然后再定義 bird 類繼承 animal 類表示鳥類,但我想討論的不是這樣的繼承(雖然這樣的繼承在 JavaScript 中也可以實現(xiàn));我想討論的繼承在 C++ 中將是,定義一個 animal 類,然后實例化了一個 myAnimal 對象。對,這在 C++ 里就是實例化,但在 JavaScript 中是作為繼承來對待的。
JavaScript 并不支持類,瀏覽器只管當(dāng)前有哪些對象,而不會額外費心思地去管,這些對象是什么 class 的,應(yīng)該具有怎樣的結(jié)構(gòu)。在我們的例子中, TICKER() 是個函數(shù)對象,我們可以對其賦值(TICKER=1),將其刪掉(TICKER=undefined),但是正因為當(dāng)前有 ticker1 和 ticker2 兩個對象是通過 new 運算符調(diào)用它而來的, TICKER() 就充當(dāng)了構(gòu)造函數(shù)的作用,而 TICKER.prototype 對象,也就充當(dāng)了類的作用。
以上就是我所了解的 JavaScript 模塊化的方法,如果您也是初學(xué)者,希望能對您有所幫助。如果有不對的地方,也勞駕您指出。
作者:一葉齋主人
出處:www.cnblogs.com/yiyezhai
相關(guān)文章
一文教你徹底學(xué)會JavaScript手寫防抖節(jié)流
其實防抖和節(jié)流不僅僅在面試中會讓大家手寫,在實際項目中也可以起到性能優(yōu)化的作用,所以還是很有必要掌握的。本文就帶大家徹底學(xué)會JavaScript手寫防抖節(jié)流,需要的可以參考一下2022-11-11Add Formatted Data to a Spreadsheet
Add Formatted Data to a Spreadsheet...2007-06-06javascript學(xué)習(xí)筆記--數(shù)字格式類型
很多人也許只知道 123,123.456,0xff 之類的數(shù)字格式。 其實 js 格式還有很多數(shù)字格式類型,比如 1., .1 這樣的,也有 .1e2 這樣的。2014-05-05JavaScript針對SPA應(yīng)用的前端優(yōu)化策略概述
前端性能優(yōu)化是Web開發(fā)中非常重要的一環(huán),優(yōu)化能夠有效地提高網(wǎng)站的加載速度和用戶體驗,而對于SPA來說,由于其特殊的加載方式和數(shù)據(jù)交互方式,其優(yōu)化策略也具有一定的特殊性,需要詳細(xì)了解可以參考下文2023-05-05javascript:google 向上向下滾動特效,兼容IE6,7,8,FF
這個代碼是我之前帶網(wǎng)上找的,因為今天再次用到,所以記錄下來,免得以后都找不到,我現(xiàn)在想去搜它的說明文檔都搜不到!2010-08-08BootStrap Progressbar 實現(xiàn)大文件上傳的進(jìn)度條的實例代碼
這篇文章主要介紹了BootStrap Progressbar 實現(xiàn)大文件上傳的進(jìn)度條的實例代碼的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-06-06IE圖片緩存document.execCommand("BackgroundImageCache",
IE6下設(shè)置背景圖片是不會被真正cache住的,就算服務(wù)器做了cache,如果想cache住只能2011-03-03JavaScript中l(wèi)ayim之整合右鍵菜單的示例代碼
這篇文章主要介紹了JavaScript中l(wèi)ayim之整合右鍵菜單的示例代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02