官术网_书友最值得收藏!

1.4 原型模式和基于原型繼承的JavaScript對象系統(tǒng)

在Brendan Eich為JavaScript設(shè)計面向?qū)ο笙到y(tǒng)時,借鑒了Self和Smalltalk這兩門基于原型的語言。之所以選擇基于原型的面向?qū)ο笙到y(tǒng),并不是因為時間匆忙,它設(shè)計起來相對簡單,而是因為從一開始Brendan Eich就沒有打算在JavaScript中加入類的概念。

在以類為中心的面向?qū)ο缶幊陶Z言中,類和對象的關(guān)系可以想象成鑄模和鑄件的關(guān)系,對象總是從類中創(chuàng)建而來。而在原型編程的思想中,類并不是必需的,對象未必需要從類中創(chuàng)建而來,一個對象是通過克隆另外一個對象所得到的。就像電影《第六日》一樣,通過克隆可以創(chuàng)造另外一個一模一樣的人,而且本體和克隆體看不出任何區(qū)別。

原型模式不單是一種設(shè)計模式,也被稱為一種編程泛型。

本節(jié)我們將首先學(xué)習(xí)第一個設(shè)計模式——原型模式。隨后會了解基于原型的Io語言,借助對Io語言的了解,我們對JavaScript的面向?qū)ο笙到y(tǒng)也將有更深的認(rèn)識。在本節(jié)的最后,我們將詳細(xì)了解JavaScript語言如何通過原型來構(gòu)建一個面向?qū)ο笙到y(tǒng)。

1.4.1 使用克隆的原型模式

從設(shè)計模式的角度講,原型模式是用于創(chuàng)建對象的一種模式,如果我們想要創(chuàng)建一個對象,一種方法是先指定它的類型,然后通過類來創(chuàng)建這個對象。原型模式選擇了另外一種方式,我們不再關(guān)心對象的具體類型,而是找到一個對象,然后通過克隆來創(chuàng)建一個一模一樣的對象。

既然原型模式是通過克隆來創(chuàng)建對象的,那么很自然地會想到,如果需要一個跟某個對象一模一樣的對象,就可以使用原型模式。

假設(shè)我們在編寫一個飛機大戰(zhàn)的網(wǎng)頁游戲。某種飛機擁有分身技能,當(dāng)它使用分身技能的時候,要在頁面中創(chuàng)建一些跟它一模一樣的飛機。如果不使用原型模式,那么在創(chuàng)建分身之前,無疑必須先保存該飛機的當(dāng)前血量、炮彈等級、防御等級等信息,隨后將這些信息設(shè)置到新創(chuàng)建的飛機上面,這樣才能得到一架一模一樣的新飛機。

如果使用原型模式,我們只需要調(diào)用負(fù)責(zé)克隆的方法,便能完成同樣的功能。

原型模式的實現(xiàn)關(guān)鍵,是語言本身是否提供了clone方法。ECMAScript 5提供了Object.create方法,可以用來克隆對象。代碼如下:

        var Plane = function(){
            this.blood = 100;
            this.attackLevel = 1;
            this.defenseLevel = 1;
        };

        var plane = new Plane();
        plane.blood = 500;
        plane.attackLevel = 10;
        plane.defenseLevel = 7;

        var clonePlane = Object.create( plane );
        console.log( clonePlane.blood )         //輸出500
        console.log( clonePlane.attackLevel )   //輸出10
        console.log( clonePlane.defenseLevel )  //輸出7

在不支持Object.create方法的瀏覽器中,則可以使用以下代碼:

        Object.create = Object.create || function( obj ){
            var F = function(){};
            F.prototype = obj;

            return new F();
        }

1.4.2 克隆是創(chuàng)建對象的手段

通過上一節(jié)的代碼,我們看到了如何通過原型模式來克隆出一個一模一樣的對象。但原型模式的真正目的并非在于需要得到一個一模一樣的對象,而是提供了一種便捷的方式去創(chuàng)建某個類型的對象,克隆只是創(chuàng)建這個對象的過程和手段。

在用Java等靜態(tài)類型語言編寫程序的時候,類型之間的解耦非常重要。依賴倒置原則提醒我們創(chuàng)建對象的時候要避免依賴具體類型,而用new XXX創(chuàng)建對象的方式顯得很僵硬。工廠方法模式和抽象工廠模式可以幫助我們解決這個問題,但這兩個模式會帶來許多跟產(chǎn)品類平行的工廠類層次,也會增加很多額外的代碼。

原型模式提供了另外一種創(chuàng)建對象的方式,通過克隆對象,我們就不用再關(guān)心對象的具體類型名字。這就像一個仙女要送給三歲小女孩生日禮物,雖然小女孩可能還不知道飛機或者船怎么說,但她可以指著商店櫥柜里的飛機模型說“我要這個”。

當(dāng)然在JavaScript這種類型模糊的語言中,創(chuàng)建對象非常容易,也不存在類型耦合的問題。從設(shè)計模式的角度來講,原型模式的意義并不算大。但JavaScript本身是一門基于原型的面向?qū)ο笳Z言,它的對象系統(tǒng)就是使用原型模式來搭建的,在這里稱之為原型編程范型也許更合適。

1.4.3 體驗Io語言

前面說過,原型模式不僅僅是一種設(shè)計模式,也是一種編程范型。JavaScript就是使用原型模式來搭建整個面向?qū)ο笙到y(tǒng)的。在JavaScript語言中不存在類的概念,對象也并非從類中創(chuàng)建出來的,所有的JavaScript對象都是從某個對象上克隆而來的。

對于習(xí)慣了以類為中心語言的人來說,也許一時不容易理解這種基于原型的語言。即使是對于JavaScript語言的熟練使用者而言,也可能會有一種“不識廬山真面目,只緣身在此山中”的感覺。事實上,使用原型模式來構(gòu)造面向?qū)ο笙到y(tǒng)的語言遠(yuǎn)非僅有JavaScript一家。

JavaScript基于原型的面向?qū)ο笙到y(tǒng)參考了Self語言和Smalltalk語言,為了搞清JavaScript中的原型,我們本該尋根溯源去瞧瞧這兩門語言。但由于這兩門語言距離現(xiàn)在實在太遙遠(yuǎn),我們不妨轉(zhuǎn)而了解一下另外一種輕巧又基于原型的語言——Io語言。

Io語言在2002年由Steve Dekorte發(fā)明。可以從http://iolanguage.com下載到Io語言的解釋器,安裝好之后打開Io解釋器,輸入經(jīng)典的“Hello World”程序。解釋器打印出了Hello World的字符串,這說明我們已經(jīng)可以使用Io語言來編寫一些小程序了,如圖1-1所示。

作為一門基于原型的語言,Io中同樣沒有類的概念,每一個對象都是基于另外一個對象的克隆。

就像吸血鬼的故事里必然有一個吸血鬼祖先一樣,既然每個對象都是由其他對象克隆而來的,那么我們猜測Io語言本身至少要提供一個根對象,其他對象都發(fā)源于這個根對象。這個猜測是正確的,在Io中,根對象名為Object。

這一節(jié)我們依然拿動物世界的例子來講解Io語言。在下面的代碼中,通過克隆根對象Object,就可以得到另外一個對象Animal。雖然Animal是以大寫開頭的,但是記住Io中沒有類,Animal跟所有的數(shù)據(jù)一樣都是對象。

        Animal := Object clone    //克隆動物對象

現(xiàn)在通過克隆根對象Object得到了一個新的Animal對象,所以O(shè)bject就被稱為Animal的原型。目前Animal對象和它的原型Object對象一模一樣,還沒有任何屬于它自己方法和能力。我們假設(shè)在Io的世界里,所有的動物都會發(fā)出叫聲,那么現(xiàn)在就給Animal對象添加makeSound方法吧。代碼如下:

        Animal makeSound := method( "animal makeSound " print );

好了,現(xiàn)在所有的動物都能夠發(fā)出叫聲了,那么再來繼續(xù)創(chuàng)建一個Dog對象。顯而易見,Animal對象可以作為Dog對象的原型,Dog對象從Animal對象克隆而來:

        Dog := Animal clone

可以確定,Dog一定懂得怎么吃食物,所以接下來給Dog對象添加eat方法:

        Dog eat := method( "dog eat " print );

現(xiàn)在已經(jīng)完成了整個動物世界的構(gòu)建,通過一次次克隆,Io的對象世界里不再只有形單影只的根對象Object,而是多了兩個新的對象:Animal對象和Dog對象。其中Dog的原型是Animal, Animal對象的原型是Object。最后我們來測試Animal對象和Dog對象的功能。

先嘗試調(diào)用Animal的makeSound方法,可以看到,動物順利發(fā)出了叫聲:

        Animal makeSound      //輸出:animal makeSound

然后再調(diào)用Dog的eat方法,同樣我們也看到了預(yù)期的結(jié)果:

        Dog eat    //輸出:dog eat

1.4.4 原型編程范型的一些規(guī)則

從上一節(jié)的講解中,我們看到了如何在Io語言中從無到有地創(chuàng)建一些對象。跟使用“類”的語言不一樣的地方是,Io語言中最初只有一個根對象Object,其他所有的對象都克隆自另外一個對象。如果A對象是從B對象克隆而來的,那么B對象就是A對象的原型。

在上一小節(jié)的例子中,Object是Animal的原型,而Animal是Dog的原型,它們之間形成了一條原型鏈。這個原型鏈?zhǔn)呛苡杏锰幍模?dāng)我們嘗試調(diào)用Dog對象的某個方法時,而它本身卻沒有這個方法,那么Dog對象會把這個請求委托給它的原型Animal對象,如果Animal對象也沒有這個屬性,那么請求會順著原型鏈繼續(xù)被委托給Animal對象的原型Object對象,這樣一來便能得到繼承的效果,看起來就像Animal是Dog的“父類”, Object是Animal的“父類”。

這個機制并不復(fù)雜,卻非常強大,Io和JavaScript一樣,基于原型鏈的委托機制就是原型繼承的本質(zhì)。

我們來進行一些測試。在Io的解釋器中執(zhí)行Dog makeSound時,Dog對象并沒有makeSound方法,于是把請求委托給了它的原型Animal對象,而Animal對象是有makeSound方法的,所以該條語句可以順利得到輸出,如圖1-2所示。

現(xiàn)在我們明白了原型編程中的一個重要特性,即當(dāng)對象無法響應(yīng)某個請求時,會把該請求委托給它自己的原型。

最后整理一下本節(jié)的描述,我們可以發(fā)現(xiàn)原型編程范型至少包括以下基本規(guī)則。

? 所有的數(shù)據(jù)都是對象。

? 要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它。

? 對象會記住它的原型。

? 如果對象無法響應(yīng)某個請求,它會把這個請求委托給它自己的原型。

1.4.5 JavaScript中的原型繼承

剛剛我們已經(jīng)體驗過同樣是基于原型編程的Io語言,也已經(jīng)了解了在Io語言中如何通過原型鏈來實現(xiàn)對象之間的繼承關(guān)系。在原型繼承方面,JavaScript的實現(xiàn)原理和Io語言非常相似,JavaScript也同樣遵守這些原型編程的基本規(guī)則。

? 所有的數(shù)據(jù)都是對象。

? 要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它。

? 對象會記住它的原型。

? 如果對象無法響應(yīng)某個請求,它會把這個請求委托給它自己的原型。

下面我們來分別討論JavaScript是如何在這些規(guī)則的基礎(chǔ)上來構(gòu)建它的對象系統(tǒng)的。

1.所有的數(shù)據(jù)都是對象

JavaScript在設(shè)計的時候,模仿Java引入了兩套類型機制:基本類型和對象類型。基本類型包括undefined、number、boolean、string、function、object。從現(xiàn)在看來,這并不是一個好的想法。

按照J(rèn)avaScript設(shè)計者的本意,除了undefined之外,一切都應(yīng)是對象。為了實現(xiàn)這一目標(biāo),number、boolean、string這幾種基本類型數(shù)據(jù)也可以通過“包裝類”的方式變成對象類型數(shù)據(jù)來處理。

我們不能說在JavaScript中所有的數(shù)據(jù)都是對象,但可以說絕大部分?jǐn)?shù)據(jù)都是對象。那么相信在JavaScript中也一定會有一個根對象存在,這些對象追根溯源都來源于這個根對象。

事實上,JavaScript中的根對象是Object.prototype對象。Object.prototype對象是一個空的對象。我們在JavaScript遇到的每個對象,實際上都是從Object.prototype對象克隆而來的,Object.prototype對象就是它們的原型。比如下面的obj1對象和obj2對象:

        var obj1 = new Object();
        var obj2 = {};

可以利用ECMAScript 5提供的Object.getPrototypeOf來查看這兩個對象的原型:

        console.log( Object.getPrototypeOf( obj1 ) === Object.prototype );    //輸出:true
        console.log( Object.getPrototypeOf( obj2 ) === Object.prototype );    //輸出:true

2.要得到一個對象,不是通過實例化類,而是找到一個對象作為原型并克隆它

在Io語言中,克隆一個對象的動作非常明顯,我們可以在代碼中清晰地看到clone的過程。比如以下代碼:

        Dog :=  Animal clone

但在JavaScript語言里,我們并不需要關(guān)心克隆的細(xì)節(jié),因為這是引擎內(nèi)部負(fù)責(zé)實現(xiàn)的。我們所需要做的只是顯式地調(diào)用var obj1 = new Object()或者var obj2 = {}。此時,引擎內(nèi)部會從Object.prototype上面克隆一個對象出來,我們最終得到的就是這個對象。

再來看看如何用new運算符從構(gòu)造器中得到一個對象,下面的代碼我們再熟悉不過了:

        function Person( name ){
            this.name = name;
        };

        Person.prototype.getName = function(){
            return this.name;
        };

        var a = new Person( 'sven' )

        console.log( a.name );    // 輸出:sven
        console.log( a.getName() );     // 輸出:sven
        console.log( Object.getPrototypeOf( a ) === Person.prototype );     // 輸出:true

在JavaScript中沒有類的概念,這句話我們已經(jīng)重復(fù)過很多次了。但剛才不是明明調(diào)用了new Person()嗎?

在這里Person并不是類,而是函數(shù)構(gòu)造器,JavaScript的函數(shù)既可以作為普通函數(shù)被調(diào)用,也可以作為構(gòu)造器被調(diào)用。當(dāng)使用new運算符來調(diào)用函數(shù)時,此時的函數(shù)就是一個構(gòu)造器。用new運算符來創(chuàng)建對象的過程,實際上也只是先克隆Object.prototype對象,再進行一些其他額外操作的過程。 JavaScript是通過克隆Object.prototype來得到新的對象,但實際上并不是每次都真正地克隆了一個新的對象。從內(nèi)存方面的考慮出發(fā),JavaScript還做了一些額外的處理,具體細(xì)節(jié)可以參閱周愛民老師編著的《JavaScript語言精髓與編程實踐》。這里不做深入討論,我們暫且把創(chuàng)建對象的過程看成完完全全的克隆。

在Chrome和Firefox等向外暴露了對象__proto__屬性的瀏覽器下,我們可以通過下面這段代碼來理解new運算的過程:

        function Person( name ){
            this.name = name;
        };

        Person.prototype.getName = function(){
            return this.name;
        };

        var objectFactory = function(){
            var obj = new Object(),    // 從Object.prototype上克隆一個空的對象
              Constructor = [].shift.call( arguments );    // 取得外部傳入的構(gòu)造器,此例是Person
            obj.__proto__ = Constructor.prototype;    // 指向正確的原型
            var ret = Constructor.apply( obj, arguments );    // 借用外部傳入的構(gòu)造器給obj設(shè)置屬性

            return typeof ret === 'object' ? ret : obj;     // 確保構(gòu)造器總是會返回一個對象
        };

        var a = objectFactory( Person, 'sven' );

        console.log( a.name );    // 輸出:sven
        console.log( a.getName() );     // 輸出:sven
        console.log( Object.getPrototypeOf( a ) === Person.prototype );      // 輸出:true

我們看到,分別調(diào)用下面兩句代碼產(chǎn)生了一樣的結(jié)果:

        var a = objectFactory( A, 'sven' );
        var a = new A( 'sven' );

3.對象會記住它的原型

如果請求可以在一個鏈條中依次往后傳遞,那么每個節(jié)點都必須知道它的下一個節(jié)點。同理,要完成Io語言或者JavaScript語言中的原型鏈查找機制,每個對象至少應(yīng)該先記住它自己的原型。

目前我們一直在討論“對象的原型”,就JavaScript的真正實現(xiàn)來說,其實并不能說對象有原型,而只能說對象的構(gòu)造器有原型。對于“對象把請求委托給它自己的原型”這句話,更好的說法是對象把請求委托給它的構(gòu)造器的原型。那么對象如何把請求順利地轉(zhuǎn)交給它的構(gòu)造器的原型呢?

JavaScript給對象提供了一個名為__proto__的隱藏屬性,某個對象的__proto__屬性默認(rèn)會指向它的構(gòu)造器的原型對象,即{Constructor}.prototype。在一些瀏覽器中,__proto__被公開出來,我們可以在Chrome或者Firefox上用這段代碼來驗證:

        var a = new Object();
        console.log ( a.__proto__=== Object.prototype );    // 輸出:true

實際上,__proto__就是對象跟“對象構(gòu)造器的原型”聯(lián)系起來的紐帶。正因為對象要通過__proto__屬性來記住它的構(gòu)造器的原型,所以我們用上一節(jié)的objectFactory函數(shù)來模擬用new創(chuàng)建對象時,需要手動給obj對象設(shè)置正確的__proto__指向。

        obj.__proto__ = Constructor.prototype;

通過這句代碼,我們讓obj.__proto__ 指向Person.prototype,而不是原來的Object.prototype。

4.如果對象無法響應(yīng)某個請求,它會把這個請求委托給它的構(gòu)造器的原型

這條規(guī)則即是原型繼承的精髓所在。從對Io語言的學(xué)習(xí)中,我們已經(jīng)了解到,當(dāng)一個對象無法響應(yīng)某個請求的時候,它會順著原型鏈把請求傳遞下去,直到遇到一個可以處理該請求的對象為止。

JavaScript的克隆跟Io語言還有點不一樣,Io中每個對象都可以作為原型被克隆,當(dāng)Animal對象克隆自O(shè)bject對象,Dog對象又克隆自Animal對象時,便形成了一條天然的原型鏈,如圖1-3所示。

而在JavaScript中,每個對象都是從Object.prototype對象克隆而來的,如果是這樣的話,我們只能得到單一的繼承關(guān)系,即每個對象都繼承自O(shè)bject.prototype對象,這樣的對象系統(tǒng)顯然是非常受限的。

實際上,雖然JavaScript的對象最初都是由Object.prototype對象克隆而來的,但對象構(gòu)造器的原型并不僅限于Object.prototype上,而是可以動態(tài)指向其他對象。這樣一來,當(dāng)對象a需要借用對象b的能力時,可以有選擇性地把對象a的構(gòu)造器的原型指向?qū)ο骲,從而達到繼承的效果。下面的代碼是我們最常用的原型繼承方式:

        var obj = { name: 'sven' };

        var A = function(){};
        A.prototype = obj;

        var a = new A();
        console.log( a.name );    // 輸出:sven

我們來看看執(zhí)行這段代碼的時候,引擎做了哪些事情。

? 首先,嘗試遍歷對象a中的所有屬性,但沒有找到name這個屬性。

? 查找name屬性的這個請求被委托給對象a的構(gòu)造器的原型,它被a.__proto__ 記錄著并且指向A.prototype,而A.prototype被設(shè)置為對象obj。

? 在對象obj中找到了name屬性,并返回它的值。

當(dāng)我們期望得到一個“類”繼承自另外一個“類”的效果時,往往會用下面的代碼來模擬實現(xiàn):

        var A = function(){};
        A.prototype = { name: 'sven' };

        var B = function(){};
        B.prototype = new A();

        var b = new B();
        console.log( b.name );    // 輸出:sven

再看這段代碼執(zhí)行的時候,引擎做了什么事情。

? 首先,嘗試遍歷對象b中的所有屬性,但沒有找到name這個屬性。

? 查找name屬性的請求被委托給對象b的構(gòu)造器的原型,它被b.__proto__記錄著并且指向B.prototype,而B.prototype被設(shè)置為一個通過new A()創(chuàng)建出來的對象。

? 在該對象中依然沒有找到name屬性,于是請求被繼續(xù)委托給這個對象構(gòu)造器的原型A.prototype。

? 在A.prototype中找到了name屬性,并返回它的值。

和把B.prototype直接指向一個字面量對象相比,通過B.prototype = new A()形成的原型鏈比之前多了一層。但二者之間沒有本質(zhì)上的區(qū)別,都是將對象構(gòu)造器的原型指向另外一個對象,繼承總是發(fā)生在對象和對象之間。

最后還要留意一點,原型鏈并不是無限長的。現(xiàn)在我們嘗試訪問對象a的address屬性。而對象b和它構(gòu)造器的原型上都沒有address屬性,那么這個請求會被最終傳遞到哪里呢?

實際上,當(dāng)請求達到A.prototype,并且在A.prototype中也沒有找到address屬性的時候,請求會被傳遞給A.prototype的構(gòu)造器原型Object.prototype,顯然Object.prototype中也沒有address屬性,但Object.prototype的原型是null,說明這時候原型鏈的后面已經(jīng)沒有別的節(jié)點了。所以該次請求就到此打住,a.address返回undefined。

        a.address        // 輸出:undefined

1.4.6 原型繼承的未來

設(shè)計模式在很多時候其實都體現(xiàn)了語言的不足之處。Peter Norvig曾說,設(shè)計模式是對語言不足的補充,如果要使用設(shè)計模式,不如去找一門更好的語言。這句話非常正確。不過,作為Web前端開發(fā)者,相信JavaScript在未來很長一段時間內(nèi)都是唯一的選擇。雖然我們沒有辦法換一門語言,但語言本身也在發(fā)展,說不定哪天某個模式在JavaScript中就已經(jīng)是天然的存在,不再需要拐彎抹角來實現(xiàn)。比如Object.create就是原型模式的天然實現(xiàn)。使用Object.create來完成原型繼承看起來更能體現(xiàn)原型模式的精髓。目前大多數(shù)主流瀏覽器都提供了Object.create方法。

但美中不足是在當(dāng)前的JavaScript引擎下,通過Object.create來創(chuàng)建對象的效率并不高,通常比通過構(gòu)造函數(shù)創(chuàng)建對象要慢。此外還有一些值得注意的地方,比如通過設(shè)置構(gòu)造器的prototype來實現(xiàn)原型繼承的時候,除了根對象Object.prototype本身之外,任何對象都會有一個原型。而通過Object.create( null )可以創(chuàng)建出沒有原型的對象。

另外,ECMAScript 6帶來了新的Class語法。這讓JavaScript看起來像是一門基于類的語言,但其背后仍是通過原型機制來創(chuàng)建對象。通過Class創(chuàng)建對象的一段簡單示例代碼這段代碼來自http://jurberg.github.io/blog/2014/07/12/javascript-prototype/。如下所示:

        class Animal {
          constructor(name) {
            this.name = name;
          }
          getName() {
            return this.name;
          }
        }

        class Dog extends Animal {
          constructor(name) {
            super(name);
          }
          speak() {
            return "woof";
          }
        }

        var dog = new Dog("Scamp");
        console.log(dog.getName() + ' says ' + dog.speak());

1.4.7 小結(jié)

本節(jié)講述了本書的第一個設(shè)計模式——原型模式。原型模式是一種設(shè)計模式,也是一種編程泛型,它構(gòu)成了JavaScript這門語言的根本。本節(jié)首先通過更加簡單的Io語言來引入原型模式的概念,隨后學(xué)習(xí)了JavaScript中的原型模式。原型模式十分重要,和JavaScript開發(fā)者的關(guān)系十分密切。通過原型來實現(xiàn)的面向?qū)ο笙到y(tǒng)雖然簡單,但能力同樣強大。

主站蜘蛛池模板: 鹤庆县| 儋州市| 襄樊市| 上犹县| 象山县| 铁力市| 云龙县| 绥德县| 绥棱县| 彭山县| 江孜县| 重庆市| 南投市| 五常市| 宝山区| 登封市| 红河县| 南投县| 乌拉特中旗| 马尔康县| 霍邱县| 卢氏县| 凤山市| 若羌县| 军事| 报价| 陇川县| 宿州市| 海宁市| 北辰区| 冷水江市| 江陵县| 鄂伦春自治旗| 林甸县| 大竹县| 泰和县| 兰州市| 台州市| 泽州县| 南部县| 肇源县|