一個簡單的、物件導向的javascript基礎架構
如果以後公司再能讓我獨立做一套新的完整系統,那麼我肯定會為這個系統再寫一個前端架構,那麼我到底該如何寫這個架構呢? 在我以前的部落格裡我給大家展示了一個我自己寫的架構,由於當時時間很緊張,做之前幾乎沒有完整的思考過我到底該如何去寫這個架構,所以事後對於這個架構我有很多遺憾之處,當我重構過一次代碼後我就沒再做過任何重構作業的工作,因為我根本不想再去給它修修補補了,之所以有這個想法,就是我對我寫的那個架構的基礎架構不滿意。 為什麼不滿意這個基礎架構了?我們先來看看我當時封裝架構的方式: 複製代碼(function(window,document){ var o1 ={},o2={}; var outerObj = { o1:o1, o2:o2 } window.outerObj = outerObj;})(window,document,undefined)複製代碼 這段代碼是我參考jQuery的編寫方式,現在回頭看看,這個編寫方式無非就是用匿名函數把內部代碼保護起來,除此之外真的沒有用到什麼javascript語言其他特性。不過當時這麼寫還是有我自己的考慮的,主要是從下面這三個角度思考: 要提高網站效能就得盡量減少頁面載入時候的請求個數,因此外部的javascript檔案越少越好;單個請求的大小也要盡量最少,因此頁面裡javascript代碼要盡量精簡;要向jQuery學習,在頁面裡編寫的代碼應該有固定的套路,只要是兩個頁面都會使用javascript代碼都應該遷移到自己編寫的javascript庫裡,其次,頁面的javascript開發裡容易出錯的代碼都應該由javascript庫來完成 因此我開發javascript庫的時候將大量代碼都遷移到一個外部檔案裡,這些代碼有的是基礎性的代碼,例如一些工具類,grid的構建代碼,遮罩功能等,還有些代碼是業務代碼例如:加密解密等,這些都集中在了一個外部檔案,由於自己原架構的結構只是將這些代碼通過匿名函數封裝起來,而沒有讓基礎性代碼和業務代碼解耦的方式,所以當時編寫的javascript庫就是一個大雜燴,很多東西交織在一起,這使得自己維護代碼的時候不是很科學了,經常變成寫入程式碼。 總而言之,我之前的javascript庫的基礎結構沒有很好的擴充性和伸縮性,它沒有將不同類型代碼隔離出來的能力,所以我需要一個新的javascript基礎架構,這樣我以後再去開發一個javascript庫,這個基礎架構使得庫更加的健壯。 我最近做了一些前端的項目,這個項目裡單個頁面都是非常複雜的,功能非常多,單個頁面的javascript業務代碼少則幾百行,多則上千行,因此我們前端應用裡有一個很大的公用庫,但是到了每個頁面又得單獨寫一個外部javascript檔案做相應的業務處理,下面是這個項目javascript代碼書寫的基本結構: 複製代碼 var opts = { version:"1.0.0", name:"sharpxiajun" }; (function(opts){ function Clazz(){ return this.init(arguments); } Clazz.fn = Clazz.prototype = { init:function(opts){ this.settings = opts; return this; }, testInit:function(){ // 直接列印對象 console.log(this.settings); // 遍曆對象輸出 for (var o in this.settings){ console.log(this.settings[o]); } return this; } } window.$ = new Clazz(opts) })(opts); // 測試 $.testInit();複製代碼 運行結果如下所示: 不管我們使用外部公用類庫,還是每個頁面對應的javascript外部檔案,都採用這個結構,仔細分析下這個代碼,它和我之前寫的javascript庫的結構並沒有高明之處,只不過我之前的javascript庫是直接使用javascript的對象,而這個結構無非換成了物件導向的寫法,而這個物件導向其實是不好使用繼承的物件導向,是個孤立的對象,因此實際開發裡我們要區別不同的功能模組,只得建立多個不同的功能對象,最後整個應用裡會有N多個相互獨立的功能對象,這其實和我原來庫的寫法有著同樣的不易擴充的問題。而且這個代碼有個讓我開發時候很不習慣的問題,就是在具體頁面開發裡,我必須先要構建一個參數對象,並把對象傳到外部檔案的介面裡,如果你沒有提前構造參數對象,那麼外部javascript代碼就會出錯。 不過這兩種寫法的差異讓我對編寫javascript庫有了新的想法,這個想法具體內容如下: 一個web前端應用裡,排除一些公用的庫例如我必用的jQuery,可能還有時間控制項的庫(我以後做web前端估計都會盡量讓我的外部庫最少,像jQuery,requireJs,seajs這樣的庫我基本是毫無保留的使用,像什麼eaysui,jqgrid,extjs這樣的庫我會想盡辦法捨棄),其他的javascript代碼都是程式員要自己編寫的,程式員自己寫的代碼不管它是業務代碼還是通用代碼都應該是一個整體,這個整體的表現就是它們都可以用一個對象進行輸出,看看我上面講的兩種寫庫的基礎結構,它們的共同問題要麼就是通用代碼和業務代碼交織,耦合,要麼就是相互獨立,關係僵硬。 此外,生產上能把javascript合并成少量檔案也是非常重要的,上面的第二種庫的寫法(把頁面的業務代碼抽取到外部檔案)目的之一就是讓檔案合并比較容易,但是這種堆砌式的合并檔案總讓我感覺有點不是很舒服的味道。我覺得對於複雜頁面單獨一個javascript外部檔案有很多好處,這個做法還是不能捨棄的,但是這個外部檔案最好和超大的公用庫有一個邏輯關係,這個關係最好像jQuery原始庫和它的外掛程式之間的關係,如果有這樣的關係我們再合并檔案,這個做法感覺就會好多了。 最後,我自己寫庫的做法沒有使用物件導向編程,使用的是javascript對象本身的特點,這個如果換到物件導向編程裡就是類的靜態變數方案,而另外一種寫法則是執行個體化對象的實現方案,我覺得這兩種寫法都有可取之處,也有不足地方,最好我們設計的庫應該相容這兩個機制。 下面就是我根據上面思考寫的新的基礎javascript基礎架構模型,代碼如下: 複製代碼(function(window,document){ function Clazz(){ return this.init(arguments); } Clazz.fn = Clazz.prototype = { init:function(opts){ this.settings = opts; return this; }, testInit:function(){ // 直接列印對象 console.log(this.settings); // 遍曆對象輸出 for (var o in this.settings){ console.log(this.settings[o]); } return this; } }; Clazz.addStaticMethod = function(nmSpace,obj,ftn){ if (!Clazz[nmSpace]){Clazz[nmSpace] = {}} for (var i in obj){ Clazz[nmSpace][i] = obj[i]; } if (ftn) {ftn()} } Clazz.addObjectMethod = function(nmSpace,obj,ftn){ if (!Clazz.fn[nmSpace]){Clazz.fn[nmSpace] = {}} for (var i in obj){ Clazz.fn[nmSpace][i] = obj[i]; } if (ftn) {ftn()} } window.Clazz = Clazz;})(window,document,undefined)var opts = { version:"1.0.0", name:"sharpxiajun"};Clazz.addStaticMethod("myStatic",{ sClz:"static", staticFtn:function(){ console.log(Clazz["myStatic"].sClz); }},function(){ console.log("Add Static Method End!!!!!!!");})Clazz.addObjectMethod("myFirst",{ sParam:"sharp", ftn01:function(s){ this.sParam = s; return this; }, ftn02:function(){ console.log("sParam:" + this.sParam); return this; }},function(){ console.log("Add Object Method End!!!!!!!");})var $ = new Clazz(opts);// 測試一$.testInit();// 測試二console.log($.myFirst.sParam);$.myFirst.ftn01("My God!!!").ftn02();// 測試三console.log(Clazz.myStatic.sClz);Clazz.myStatic.staticFtn();複製代碼 頁面運行結果如下所示: 這個代碼裡我在匿名函數裡返回的是類(javascript裡其實沒有類的概念,但是有建構函式,所以javascript裡的建構函式承擔了類的作用,所以這裡提javascript裡的類應該是沒問題的),而不是已經執行個體化好的對象。這樣返回的東西既可以有靜態方法和屬性又有屬於對象的方法和屬性了。 該結構裡有一個Clazz.addStaticMethod方法,它的作用是給定義的類添加靜態方法,這個方法我設計了三個參數,第一個參數是個字串,業務函數就是靜態方法的範圍,看下面執行個體代碼: console.log(Clazz.myStatic.sClz); Clazz.myStatic.staticFtn(); 這樣就等於給靜態變數一個保護,有人會問如果我們不傳第一個參數怎麼辦?認真的童鞋注意,現在我給出的代碼只是想表達我的想法,到了生產實現時候該方法會更加豐滿點,那時如果使用者不傳範圍欄位,那麼添加的靜態方法和屬性就是直接屬於類本身的。 第二個參數就是具體要添加的靜態屬性和靜態方法,這裡我用的是物件類型。 第三個參數是個回呼函數,當靜態方式添加成功後調用,當然使用者也可以不傳,加個回呼函數參數,是我覺得在設計javascript方法時候都應該給一個回呼函數,這就是在運用事件驅動編程的思想,這其實為自己留有餘地,這麼做常常會在你意想不到的時候發揮重要作用。 方法Clazz.addObjectMethod是給對象添加方法和屬性的,這些方法和屬性石賦予給原型對象的,參數類型和addStaticMethod相同,這裡就不在累述了。 使用者在使用addObjectMethod方法時候要注意this指標的運用,在每個要添加到對象的方法最後加上一個return this,這麼做好處就是每個對象的傳回值都是對象本身,這樣我們就可以讓我們的庫擁有和jQuery一樣的連綴寫法,例如下面的代碼: console.log($.myFirst.sParam); $.myFirst.ftn01("My God!!!").ftn02(); 在生產開發裡,我們可以把公用的javascript代碼直接寫到庫裡,頁面裡的業務代碼則直接通過擴充返回類的靜態方法和屬性以及對象方法和屬性進行擴充。 上面這個結構基本達到我的需要了,如果我以後用javascriptMVC思想開發前端我估計這個基本結構已經夠用,當然還要做些代碼健壯性的處理,如果是傳統前端頁面開發估計還會有一定修改,傳統網站的頁面開發都是服務端和html混搭的,例如jsp,velocity檔案,因此有很多重要資料都是服務端變數產生的,我們時常要把這些變數作為參數傳給外部javascript檔案,每個頁面裡的參數是不一樣的,所以必須有個對象專門接收這個對象。這個好做,因此這裡就不累述了。 上面這個結構使用了物件導向繼承機制,原始庫是父物件,而業務javascript檔案則是子類了,不過這個子類是用命名空間來區分的。 不過有時候我們可能想冒險替換整個父物件的內容,這個需求我想在平時開發裡並不常見,不過我還是寫了一個這樣的方法,下面是我改進的代碼,具體如下: 複製代碼(function(window,document){ function Clazz(){ return this._init(arguments); } Clazz.prototype = { _init:function(opts){ this.settings = opts; return this; }, testInit:function(){ // 直接列印對象 console.log(this.settings); // 遍曆對象輸出 for (var o in this.settings){ console.log(this.settings[o]); } return this; } }; Clazz.addStaticMethod = function(nmSpace,obj,ftn){ if (!Clazz[nmSpace]){Clazz[nmSpace] = {}} if (Object.prototype.toString.apply(obj) == "[object Object]"){ for (var i in obj){ Clazz[nmSpace][i] = obj[i]; } window.Clazz = Clazz; } if (ftn) {ftn()} } Clazz.addObjectMethod = function(nmSpace,obj,ftn){ if (!Clazz.prototype[nmSpace]){Clazz.prototype[nmSpace] = {}} if (Object.prototype.toString.apply(obj) == "[object Object]"){ for (var i in obj){ Clazz.prototype[nmSpace][i] = obj[i]; } window.Clazz = Clazz; } if (ftn) {ftn()} } /*Clazz.newStaticMethod(){//todo..........}*/ Clazz.newObjectMethod = function(obj,ftn){ if (Object.prototype.toString.apply(obj) == "[object Object]"){ var tmpInit = Clazz.prototype._init; Clazz.prototype = obj; Clazz.prototype._init = tmpInit; window.Clazz = Clazz; } if (ftn) {ftn()} } window.Clazz = Clazz;})(window,document,undefined)var opts = { version:"1.0.0", name:"sharpxiajun"};Clazz.addStaticMethod("myStatic",{ sClz:"static", staticFtn:function(){ console.log(Clazz["myStatic"].sClz); }},function(){ console.log("Add Static Method End!!!!!!!");})Clazz.myStatic.staticFtn();Clazz.newObjectMethod({ newver:"1.0.3", testNewFtn:function(){ console.log(this.newver); return this; }},function(){ console.log("New Create Prototype Object End !!!!!!");});Clazz.addObjectMethod("myFirst",{ sParam:"sharp", ftn01:function(s){ this.sParam = s; return this; }, ftn02:function(){ console.log("sParam:" + this.sParam); return this; }},function(){ console.log("Add Object Method End!!!!!!!");})var $ = new Clazz();console.log("================================");console.log($.newver);$.testNewFtn().myFirst.ftn01("XXXX").ftn02();console.log("================================");複製代碼 運行結果如下所示: 上面的代碼做了一定最佳化,代碼裡我只給出了替換原型的方法,沒有提供替換靜態方法,這是因為替換原型還是有點意義的,替換靜態變數其實就是替換整個類了,如果這麼幹,我這個結構還有啥意義哦。