上一集中,重點介紹了誰動了你的程式碼。這裡先總結一下:我們們的程式碼從敲下來到執行出結果,經歷了兩個階段:分析期與執行期。在分析期,JavaScript分析器悄悄動了我們的程式碼;在執行期,JavaScript又按照自己的一套機制進行變數尋找。我們的程式碼是如何被動了手腳的,相信看官你已經明白。但是前面所聊均是程式導向的,如果說只是簡單的程式導向言語,那JavaScript能夠有基本的資料型別,基本的執行單元那也差不多了。但是故事並沒有在此結束。接下來劇情的發展,那才是造成今天鞋同們困惑的地方,那們還是從故事開始。大夥不要嫌樓主囉嗦(樓主確實是個囉嗦之人),講這故事是為了讓大夥瞭解當年布大師設計JavaScript的背景,融入布大師的設計思維,你就知道JavaScript為什麼會有哪些奇怪的設計。好,故事開始了。
前幾集的故事中,我們們提到了布大師只想設計一個簡單、滿足瀏覽器進行資料檢驗的指令碼言語。當時的web應用毫無顏值,猶如白紙黑字,頂多再加點圖片。所以,你也別期待當時的布大師會想到如UI互動、動畫效果等等的設計需求。為此,從一開始布大師設計的JavaScript就是一個過程式的簡單的言語,但是布大師也不是個迂腐落後之人。c的升級版c++、讓程式設計界有點瘋狂的Java,布大師也不能視而不見,多少受點影響。於是乎,布大師想:我這JavaScript能否也玩點OOP思想呢?布大師這麼一想,一堆問題就來了,本來就沒打算搞個正式的OOP指令碼,也沒設計有class、extend,更沒有override啥的。但是今天拍腦袋一想要玩OOP,那總得在現有的設計基礎上去實現OOP三大思想(封裝、繼承、多型)吧。那我們們就看看布大師是如何給JavaScript賦予OOP的。
封裝
概念,樓主就不說了。但是你看看JavaScript定義的那些資料型別,壓根就沒class的概念。沒有類何來例項,沒有例項談何封裝?布大師翻來覆去研究已經定義的資料型別,再對比了c++、java。他發現c++、java每次建立物件都離不開呼叫建構函式。布大師靈感一來“對!繞過class直接呼叫建構函式建立物件,剛好function可以作為建構函式”。於是乎,你見到了今天JavaScript是這樣建立實現物件的:
/*** *定義建構函式 ****/ function clazz(params){ } /**通過建構函式建立例項**/ var ins=new clazz(1);
好了,建立物件的事情是解決了。但也不是說有了new就萬事大吉。碼農們都知道,有類得有類的資料成員吧。布大師也知道這個問題,那他是怎麼賦予資料成員呢?這傢伙又去挖java的idea了,他發現java例項都有this。this這個神器都可以直接訪問資料成員。於是乎,他又把this扯進了function裡面,用於封裝例項的資料成員。這樣看上去封裝的事情解決得差不多了。
/*** *定義建構函式 ****/ function clazz(p1,p2){ this.p1=p1; this.p2=p2; this.print=function(){ console.log(this.p1+this.p2); } } /**通過建構函式建立例項**/ var ins1=new clazz(1,1); /***呼叫例項成員 修改ins1.p1 看看是否影響了ins2.p1****/ ins1.p1=9; ins1.print(); //結果1+9=10 var ins2=new clazz(3,1);//結果3+1=4 ins2.print(); console.log("結論修改ins1.p1不影響ins2.p1;證明this下的定義是各自儲存副本的");
至此,JavaScript即可建立例項,有具有例項成員,看上去沒啥問題,滿足了OOP的“封裝”思想,但是布大師就是大師,他總覺得上面的程式碼還有點問題。this是屬於例項的,這意味著,掛在this下面的成員不是例項共享的(ins1、ins2分別儲存了各自的副本)。如上面p1、p2作為資料成員,各自例項分別擁有那是十分合理的。但是print這個函式也搞分別擁有似乎不合理啊。因為函式是基於棧執行的,而棧又是私有的,函式的執行其實就是在私有棧裡面跑程式碼,這裡將例項函式定義到this下面有點浪費記憶體了。布大師又腦洞大開:那我得在function上面再搞點東西來存放例項共享的成員,解決無法共享成員浪費記憶體的問題。於是乎,他在每個function建構函式裡面搞了prototype用於存放例項共享成員,每個例項實際上都擁有prototype定義的成員。
/*** *定義建構函式 ****/ function clazz(p1,p2){ this.p1=p1; this.p2=p2; this.print=function(){ console.log(this.p1+this.p2); } } /***定義例項共享成員***/ clazz.prototype.p3=function(){ console.log("come on man!"); }; /**通過建構函式建立例項**/ var ins1=new clazz(1,1); var ins2=new clazz(3,1); console.log(ins1.p3());//結果:come on man! console.log(ins2.p3());//結果:come on man! /**clazz.prototype.p3 看看是否影響了ins1 ins2**/ clazz.prototype.p3=function(){ console.log("come on girl!"); }; console.log(ins1.p3());//結果:come on girl! console.log(ins2.p3());//結果:come on girl! console.log("定義到prototype的成員是各個例項共享的!");
布大師終於圍繞著funciton搞掂了OOP的封裝思想,對funciton瞬間大愛啊!那接下來的繼承,看來布大師還是會圍繞function來動腦筋了。看官你聽說過“function是JavaScript一等公民”這個說法不?布大師在給JavaScript扯OOP的時候全靠funciton,你說它能不是一等公民嘛?那我們們看看布大師是如何又對function做手腳來實現繼承的。
繼承
說到繼承,那得先有個繼承鏈上的上帝,Java繼承鏈上的上帝是Object。毫無疑問,布大師又將JavaScript的Object封為上帝,JavaScript裡面的一切物件例項(引用型別)都是這個上帝的子民。有了上帝那還得有個維繫上帝與子民之間關係的紐帶啊。布大師又頭痛了,我這裡沒有設計extend啊,怎麼建立繼承關係呢?他又將目光投到了function身上。話說function都具有建立例項的能力了,那就在它上面再加點料讓它具有指向上帝的功能,那既不是解決了問題啊。於是他隨意一劃,在function上加了個"__proto__"用於指向當前子民的上帝。"__proto__"是加上去了,但是要不要讓程式設計師手動去編寫”function.__proto__==上帝“才建立子民與上帝的關係呢?布大師想了想,還是搞個內部自動實現吧,於是乎,你看到了每個物件都預設有個“__proto__”指向上帝Object。“__proto__”有了,同時還有個"prototype",老布看著兩玩兒,好像都差不多,也是他又想,既然都看著差不多,那麼內部實現就都合併一起吧,所以我們們定義的prototype成員實際上是被合併到了“__proto__”中。“__proto__”是隱形的而prototype則是開放給程式設計師的。
話說,布大師解決了子民如何找到上帝的問題的,但是這還不夠。子民本身有自己的成員,上帝也有上帝的成員。我們在呼叫子民成員的時候,如何兼顧呼叫上帝成員的能力呢?那這個呼叫邏輯得做下處理。通過前面的封裝故事,你已經知道了資料被封裝到了例項物件[this],我們呼叫成員的時候都是找例項[this]成員,並沒有向那個指向上帝的“__proto__”要資料啊。於是他做下呼叫邏輯的調整:先找當前例項this上是否存在被呼叫的成員,沒有則在指向上帝的"__proto__"裡查詢。於是乎繼承實現了。這裡強調一下:在function.prototype上定義的成員實際上也是被調整到了”__proto__“中,而”__proto__“中又有”__proto__“,這樣就形成了所謂的原型鏈。
看到這裡你應該明白了,既然“__proto__”是用於指向父類的,而prototype最終也是和”__proto__“合併一起,如果我們通過修改prototype的指向是不是就實現了對父類的繼承呢。正確,這也是布大師所想的,但這麼一修改就存在一個問題了,prototype本來是屬於某個fuction的,修改後指向了另外的物件,於是乎,布大師又往”__proto__“加了個constructor用於指向歸屬的function(建構函式),以表明這個”__proto__“的歸屬,如果我們通過修改prototype指向父類,還得手動將其constructor指向修正回來。於是布大師的繼承是這樣實現的。
/***定義一個人類建構函式,作為男人、女人的基類***/ function human(sex,name){ this.sex=sex; this.name=name; } /**人都會說話* ***/ human.prototype.say=function(){ console.log("人都會說話!"); console.log("我的性別是:"+this.sex); console.log("我的名字是:"+this.name); } /** * 定義一個女人建構函式 * ***/ function lady(hobby){ this.hobby=hobby; } /**原型繼承**/ lady.prototype=human.prototype; /**記得修改歸屬**/ lady.prototype.constructor=lady; var girl = new lady("化妝"); /**女人擁有了人類能說話的能力***/ girl.say();//結果 人都會說話! 我的性別是:undefined 我的名字是:undefined
結果有問題啊!女人是可以說話了,但是人類的性別、名字怎麼繼承啊?布大師又遇到了惱火的問題:原型繼承只能繼承prototype上定義的成員,無法繼承父類物件上的this成員?他又想點子了。還得拿function開刀。他想了想:建構函式裡的this成員變數是例項私有的,this.sex、this.name只歸屬與human例項的,無法歸屬於lady例項呢?除非有個偷樑換柱的辦法,執行下human函式,執行期間讓裡面的this換成lady的this。這樣藉助JavaScript的動態屬性,讓lady的this偷偷加上sex和name。於是乎他在function上加了兩個方法:call、applay。專門幹偷樑換柱的事情。上面的程式碼進一步改良:
/***定義一個人類建構函式,作為男人、女人的基類***/ function human(sex,name){ this.sex=sex; this.name=name; } /**人都會說話* ***/ human.prototype.say=function(){ console.log("人都會說話!"); console.log("我的性別是:"+this.sex); console.log("我的名字是:"+this.name); } /** * 定義一個女人建構函式 * ***/ function lady(hobby,sex,name){ //依賴javascript的動態屬性功能,通過父類建構函式的call方法實現偷樑換柱 human.call(this,sex,name); this.hobby=hobby; } /**原型繼承**/ lady.prototype=human.prototype; /**記得修改歸屬**/ lady.prototype.constructor=lady; var girl = new lady("化妝","woman","pretty-mm"); /**女人擁有了人類能說話的能力***/ girl.say();//結果 人都會說話! 我的性別是:woman 我的名字是:pretty-mm
看到這裡我覺得看官你應該明白了JavaScript的是怎麼玩物件建立,怎麼玩繼承的了。上述內容也只能說抓住要點進行故事般推理,實際詳盡的知識點肯定不是這寥寥幾段文字能夠說清的,要不然《JavaScript高階程式設計》那書怎麼能像枕頭那般。樓主只是覺得理解了這些要點,再加以進一步學習,玩轉JavaScript的oop肯定不在話下。這段是廢話了,物件導向的三大特性,我們們只講了封裝、繼承、那多型性呢?JavaScript又如何體現?
多型性
JavaScript實現OOP三大特性,多型性是弱爆的了。布大師實現的封裝、繼承,雖然確實是非主流,但至少也是實現了。而多型性,布大師就搞得有點潦草了。我估計布大師當時已經有點不耐煩了,所以也沒像前面封裝、繼承那樣用心考慮設計,再說了多型性這玩兒層次較高,JavaScript完美實現多型性思想,按當時的需求似乎也有點多餘。於是,布大師對多型性的實現,可以說比較簡單,那下面就一起聊聊JavaScript的多型性。
由於多型性是個高層次的思想,在說JavaScript多型性之前,樓主得先表達下自己對多型性的理解。樓主認為:多型性就是某個事物、行為呈現的多種狀態,在OOP程式設計裡面可以說到處都是多型性的體現。如一個Class可以根據不同的資料產生不同的instance,同一份Class不同的instance那是不是多種狀態了?如函式的重寫、過載是不是同一個函式可以產生多種不同的行為結果?這又是多型性的體現了。更甚設計模式裡面的”工廠模式“,是不是一個工廠去產生不同的例項?還有面向介面程式設計,一個介面,N份實現......。可以說在OOP程式設計裡面多型無處不在。樓主有時候甚至想:既然都無處不在了,多型性這玩兒還有必要拿出來談嘛?誰要是知道這個問題的答案,歡迎給樓主答疑。廢話了,我們們還是迴歸布大師是如何給JavaScript弄點多型性的。
話說,多型性最經典的表現就是函式的重寫、過載。重寫是函式實現的覆蓋、過載則是函式簽名的多種形式。布大師也明白,JavaScript已經被他賦予動態成員(屬性)超級能力,對於重寫,通過這個超級能力毫不費勁就實現了,如上面human的say函式,如果女人們想說點女人才說的話,那得重寫human的say函式,這個輕鬆實現:
/***定義一個人類建構函式,作為男人、女人的基類***/ function human(sex,name){ this.sex=sex; this.name=name; } /**人都會說話* ***/ human.prototype.say=function(){ console.log("人都會說話!"); console.log("我的性別是:"+this.sex); console.log("我的名字是:"+this.name); } /** * 定義一個女人建構函式 * ***/ function lady(hobby,sex,name){ //偷樑換柱 human.call(this,sex,name); this.hobby=hobby; } /**原型繼承**/ lady.prototype=human.prototype; /**記得修改歸屬**/ lady.prototype.constructor=lady; /***重寫say,讓女人們都說女人愛說的話***/ lady.prototype.say=function(){ console.log("我是女人,我當然喜歡化妝啊!"); } var girl = new lady("化妝","woman","pretty-mm"); /**女人擁有了人類能說話的能力***/ girl.say();//結果 我是女人,我當然喜歡化妝啊!
得益於動態成員(屬性)的超級能力,JavaScript實現重寫,毫不費勁。布大師也可以偷懶一下。但是過載呢?過載呢?過載呢?布大師還是得費腦子了。大師就大師,人家實現過載也是驚豔加省事(當然他這麼一搞,我們們寫JS的就不省事了)。為了應對一個函式,N個引數的過載實現,布大師又拿function開到,給function加了個arguments用於存放函式的引數,不管你有多少個引數,都可以任意在這個argments裡面獲取,你也不需要寫function(p1,p2,p3.......)這樣的程式碼,你只需要在函式體內根據arguments的長度走不通的邏輯即可。如下
function f(){ var args=arguments; if(args.length==0){ console.log("當f函式沒有傳參的邏輯"); }else if(args.length==1){ console.log("當f函式只有一個傳參的邏輯"); }else{ console.log("當f函式引數多個的邏輯"); } }
看,布大師是夠省事了,但是苦了我們們寫程式碼的了,要走一大段if...else...。那後來布大師還對此做改進啥的不?抱歉,好像沒有了,當然也可能樓主孤陋寡聞,沒有了解到的可能。各位看官要是有興趣,可以進一步研究研究。
寫了那麼多,看樣子對JavaScript尋蹤OOP之路也吹得差不多了。以上內容要是有何錯誤、紕漏,還請客觀斧正!