JavaScript尋蹤OOP之路

huang發表於2016-12-04

  上一集中,重點介紹了誰動了你的程式碼。這裡先總結一下:我們們的程式碼從敲下來到執行出結果,經歷了兩個階段:分析期與執行期。在分析期,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之路也吹得差不多了。以上內容要是有何錯誤、紕漏,還請客觀斧正!

相關文章