JavaScript高級程序設計——第6章:面向對象的程序設計

ECMA-262把對象定義爲:無序屬性的集合,其屬性能夠包含基本值、對象或者函數。能夠把ECMAScript對象想象成散列表,無非就是一組名值對,其中值能夠是數據或函數。每一個對象都是基於一個引用類型定義的,能夠是原生類型,也能夠是自定義類型。javascript

6.1 理解對象html

6.1.1 屬性類型java

ECMA-262在定義只有內部才用的特性(attribute)時,描述了屬性(property)的各類特徵。ECMA-262定義這些特性是爲了實現JavaScritpt引擎用的。所以在JavaScritpt中不能直接訪問它們。爲了表示特性是內部值,該規範把它們放在了兩對括號中。例如[[Enumerable]]。編程

ECMAScript中有兩種屬性:數據屬性、訪問器屬性。數組

6.1.2 定義多個屬性 瀏覽器

6.1.3 讀取屬性的特性安全

6.2 建立對象app

雖然Object構造函數或對象字面量均可以用來建立單個對象,但這些方式有個明顯的缺點:使用同一個接口建立不少對象,會產生大量的重複代碼。爲解決這個問題,人們開始使用工廠模式的一種變體。函數

6.2.1 工廠模式this

考慮到在ECMAScript中沒法建立類,開發人員就發明了一種函數,用函數來封裝以特定接口建立對象的細節,以下例所示:

<!DOCTYPE html>
<html>
<head>
    <title>Factory Pattern Example</title>
    <script type="text/javascript">
    
        function createPerson(name, age, job){
            var o = new Object();
            o.name = name;
            o.age = age;
            o.job = job;
            o.sayName = function(){
                alert(this.name);
            };    
            return o;
        }
        
        var person1 = createPerson("Nicholas", 29, "Software Engineer");
        var person2 = createPerson("Greg", 27, "Doctor");
        
        person1.sayName();   //"Nicholas"
        person2.sayName();   //"Greg"
    </script>
</head>
<body>

</body>
</html>

工廠模式解決了建立多個類似對象的問題,但卻沒有解決對象識別的問題。又一個新模式應運而生,

6.2.2 構造函數模式

<!DOCTYPE html>
<html>
<head>
    <title>Constructor Pattern Example</title>
    <script type="text/javascript">
    
        function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = function(){
                alert(this.name);
            };    
        }
        
        var person1 = new Person("Nicholas", 29, "Software Engineer");
        var person2 = new Person("Greg", 27, "Doctor");
        
        person1.sayName();   //"Nicholas"
        person2.sayName();   //"Greg"
        
        alert(person1 instanceof Object);  //true
        alert(person1 instanceof Person);  //true
        alert(person2 instanceof Object);  //true
        alert(person2 instanceof Person);  //true
        
        alert(person1.constructor == Person);  //true
        alert(person2.constructor == Person);  //true
        
        alert(person1.sayName == person2.sayName);  //false        
        
        
    </script>
</head>
<body>

</body>
</html>

應該注意到函數名Person使用的是大寫字母P,按照慣例構造函數始終都應該以一個大寫字母開頭,而非構造函數都應該以一個小寫字母開頭,這個做法借鑑自其餘OO語言,主要爲了區別於ECMAScript中的其它函數,由於構造函數自己也是函數,只不過能建立對象而已。以這種方式定義的構造函數是定義在Global對象(在瀏覽器中是Window對象)中的,全部對象均繼承自Object。

1)將構造函數看成函數

構造函數與其餘函數的不一樣就在於調用方式的不一樣。但也能夠像調用其餘函數的方式來調用構造函數。

2)構造函數的問題

6.2.3 原型模式

咱們建立的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象用途是包含能夠由特定類型的全部實例共享的屬性和方法。若是按照字面理解,那prototype是調用構造函數建立的那個對象實例的原型對象。使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。換句話說,沒必要在構造函數中定義對象實例的信息,而是能夠將這些信息直接添加到原型對象中。 

<!DOCTYPE html>
<html>
<head>
    <title>Prototype Pattern Example</title>
    <script type="text/javascript">
    
        function Person(){
        }
        
        Person.prototype.name = "Nicholas";
        Person.prototype.age = 29;
        Person.prototype.job = "Software Engineer";
        Person.prototype.sayName = function(){
            alert(this.name);
        };
        
        var person1 = new Person();
        person1.sayName();   //"Nicholas"
        
        var person2 = new Person();
        person2.sayName();   //"Nicholas"
      
        alert(person1.sayName == person2.sayName);  //true
        
        alert(Person.prototype.isPrototypeOf(person1));  //true
        alert(Person.prototype.isPrototypeOf(person2));  //true
        
        //only works if Object.getPrototypeOf() is available
        if (Object.getPrototypeOf){
            alert(Object.getPrototypeOf(person1) == Person.prototype);  //true
            alert(Object.getPrototypeOf(person1).name);  //"Nicholas"
        }
        
    </script>
</head>
<body>

</body>
</html>

1)理解原型對象

不管何時,只要建立了一個函數,就會根據一組特定的規則爲該函數建立一個prototype屬性,這個屬性指向函數的原型對象。在默認狀況下,全部原型對象都會自動得到一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。建立了自定義的構造函數以後,其原型對象默認只會取得constructor(構造函數)屬性,至於其它方法則都是從Object繼承而來的。

2)原型與in操做符

有兩種方式使用in操做符:單獨使用或者在for-in中使用

在單獨使用時,in操做符會在經過對象可以訪問給定屬性時返回true,不管該屬性存在於實例中,仍是原型中;在使用for-in循環時,返回的是全部可以經過對象訪問的、可枚舉的(enumerated)屬性,屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]標記的屬性)

的實例屬性也會返回(根據規定,全部開發人員定義的屬性都是可枚舉的)。

ECMAScript5的Object.keys()方法,取得對象上全部可枚舉的實例屬性。

3)更簡單的原型語法

減小沒必要要的輸入,在視覺上更好的封裝原型的功能,更常見的作法是用一個包含全部屬性和方法的對象字面量來重寫整個原型對象,以下:

 

<!DOCTYPE html>
<html>
<head>
    <title>Prototype Pattern Example</title>
    <script type="text/javascript">
            
        function Person(){
        }
        
        Person.prototype = {
            name : "Nicholas",
            age : 29,
            job: "Software Engineer",
            sayName : function () {
                alert(this.name);
            }
        };

        var friend = new Person();
        
        alert(friend instanceof Object);  //true
        alert(friend instanceof Person);  //true
        alert(friend.constructor == Person);  //false
        alert(friend.constructor == Object);  //true
        
    </script>
</head>
<body>

</body>
</html>

 

4)原型的動態性 

 隨時爲原型添加屬性和方法,而且修改可以當即在全部對象實例中體現出來,但若是是重寫整個原型對象,那麼狀況就不同了。重寫原型對象切斷了現有原型和任何以前已經存在的對象實例之間的聯繫,以前已經存在的對象實例引用的仍然是最初的原型。

5)原生對象的原型

6)原型對象的問題

省略了構造函數傳參的環節,結果全部實例在默認狀況下都將取得相同的屬性值。這還不是原型最大的問題,原型最大的問題是由其共享的本性所致使的。對於包含基本值的屬性倒也說的過去,但對於包含引用類型值的屬性來講,問題比較突出,這也是不多看到有人單獨使用原型模式的緣由所在。來看下面的例子:

<!DOCTYPE html>
<html>
<head>
    <title>Prototype Pattern Example</title>
    <script type="text/javascript">
                    
        function Person(){
        }
        
        Person.prototype = {
            constructor: Person,
            name : "Nicholas",
            age : 29,
            job : "Software Engineer",
            friends : ["Shelby", "Court"],
            sayName : function () {
                alert(this.name);
            }
        };
        
        var person1 = new Person();
        var person2 = new Person();
        
        person1.friends.push("Van");
        
        alert(person1.friends);    //"Shelby,Court,Van"
        alert(person2.friends);    //"Shelby,Court,Van"
        alert(person1.friends === person2.friends);  //true

        
    </script>
</head>
<body>

</body>
</html>

 

6.2.4 組合使用構造函數模式和原型模式

建立自定義類型最多見的方式,構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。結果每一個實例都會有本身的一份實例屬性副本,但同時又共享着對方法的引用,最大限度的節省了內存。這種混合模式還支持向構造函數傳遞參數,可謂是集兩種模式之長。下面的代碼重寫了前面的例子:

<!DOCTYPE html>
<html>
<head>
    <title>Hybrid Pattern Example</title>
    <script type="text/javascript">
                    
        function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.friends = ["Shelby", "Court"];
        }
        
        Person.prototype = {
            constructor: Person,
            sayName : function () {
                alert(this.name);
            }
        };
        
        var person1 = new Person("Nicholas", 29, "Software Engineer");
        var person2 = new Person("Greg", 27, "Doctor");
        
        person1.friends.push("Van");
        
        alert(person1.friends);    //"Shelby,Court,Van"
        alert(person2.friends);    //"Shelby,Court"
        alert(person1.friends === person2.friends);  //false
        alert(person1.sayName === person2.sayName);  //true

        
    </script>
</head>
<body>

</body>
</html>

這種混合模式是目前在ECMAScript中使用最普遍、認同度最高的一種建立自定義類型的方式。能夠說,這是用來定義引用類型的一種默認模式。

6.2.4 動態原型模式

動態原型模式致力於解決獨立的構造函數和原型的問題。經過檢查某個應該存在的方法是否有效,來決定是否須要初始化原型。

<!DOCTYPE html>
<html>
<head>
    <title>Dynamic Prototype Pattern Example</title>
    <script type="text/javascript">
                    
        function Person(name, age, job){
        
            //properties
            this.name = name;
            this.age = age;
            this.job = job;
            
            //methods
            if (typeof this.sayName != "function"){
            
                Person.prototype.sayName = function(){
                    alert(this.name);
                };
                
            }
        }

        var friend = new Person("Nicholas", 29, "Software Engineer");
        friend.sayName();


        
    </script>
</head>
<body>

</body>
</html>

6.2.4 寄生構造函數模式

在前述的幾種模式都不適用的狀況下,可使用寄生(parastic)構造函數模式。這種模式的基本思想就是建立一個函數,封裝建立對象的代碼,而後返回新建立的對象;從表面上看,這個函數又很像典型的構造函數:

<!DOCTYPE html>
<html>
<head>
    <title>Hybrid Factory Pattern Example</title>
    <script type="text/javascript">
                    
        function Person(name, age, job){
            var o = new Object();
            o.name = name;
            o.age = age;
            o.job = job;
            o.sayName = function(){
                alert(this.name);
            };    
            return o;
        }
        
        var friend = new Person("Nicholas", 29, "Software Engineer");
        friend.sayName();  //"Nicholas"


        
    </script>
</head>
<body>

</body>
</html>

除了使用new操做符並把使用的包裝函數稱做構造函數以外,這個模式其實和工廠模式如出一轍。構造函數在不返回值的狀況下,默認會返回新對象實例。而經過在構造函數的末尾添加一個return語句,能夠重寫調用構造函數時返回的值。這個模式能夠在特殊狀況下用來爲對象建立構造函數。假設咱們想建立一個具備額外方法的特殊數組。因爲不能直接修改Array構造函數,所以可使用這個模式:

<!DOCTYPE html>
<html>
<head>
    <title>Hybrid Factory Pattern Example 2</title>
    <script type="text/javascript">
                    
        function SpecialArray(){       
 
            //create the array
            var values = new Array();
            
            //add the values
            values.push.apply(values, arguments);
            
            //assign the method
            values.toPipedString = function(){
                return this.join("|");
            };
            
            //return it
            return values;        
        }
        
        var colors = new SpecialArray("red", "blue", "green");
        alert(colors.toPipedString()); //"red|blue|green"

        alert(colors instanceof SpecialArray);

        
    </script>
</head>
<body>

</body>
</html>

返回的對象與構造函數或者構造函數的原型屬性之間沒有關係。爲此不能依賴instanceof操做符來肯定對象類型。建議可使用其它模式的狀況下不要使用該模式,

6.2.5 穩妥構造函數模式

Douglas Crockford發明了Javascript中的穩妥對象(durable objects)這個概念。所謂穩妥對象,指的是沒有公共屬性,並且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境(禁止使用this、new)或防止數據被其餘應用程序(如Mashup)改動時使用。穩妥構造函數模式與寄生構造函數模式相似,但有兩點不一樣:一是新建立對象的實例方法不引用this,二是不使用new操做符調用構造函數。寄生構造函數模式相似,使用穩妥構造函數模式建立的對象與構造函數之間也沒有什麼關係。所以,instanceof操做符對這樣的對象也沒有什麼意義。

<!DOCTYPE html>
<html>
<head>
    <title>Hybrid Factory Pattern Example</title>
    <script type="text/javascript">
                    
        function Person(name, age, job){
            var o = new Object();
            o.name = name;
            o.age = age;
            o.job = job;
            o.sayName = function(){
                alert(name);
            };    
            return o;
        }
        
        var friend = Person("Nicholas", 29, "Software Engineer");
        friend.sayName();  //"Nicholas"


        
    </script>
</head>
<body>

</body>
</html>

注意:以這種方式建立的對象中,除了使用sayName()方法以外,沒有其餘辦法訪問name的值。變量friend中保存了一個穩妥對象,除了調用sayName()方法外,沒有別的方式能夠訪問它的數據成員,但也不可能有別的辦法訪問傳入到構造函數中的原始數據。

6.3 繼承
在ECMAScript中沒法實現接口繼承。ECMAScript只支持實現繼承,並且其實現繼承主要依靠原型鏈來實現的。

6.3.1 原型鏈

實現原型鏈有一種基本的模式:

<!DOCTYPE html>
<html>
<head>
    <title>Prototype Chaining Example</title>
    <script type="text/javascript">
                    
        function SuperType(){
            this.property = true;
        }
        
        SuperType.prototype.getSuperValue = function(){
            return this.property;
        };
        
        function SubType(){
            this.subproperty = false;
        }
        
        //inherit from SuperType
        SubType.prototype = new SuperType();
        
        SubType.prototype.getSubValue = function (){
            return this.subproperty;
        };
        
        var instance = new SubType();
        alert(instance.getSuperValue());   //true 此外要注意instance.constructor如今指向的是SuperType
       
        alert(instance instanceof Object);      //true
        alert(instance instanceof SuperType);   //true
        alert(instance instanceof SubType);     //true

        alert(Object.prototype.isPrototypeOf(instance));    //true
        alert(SuperType.prototype.isPrototypeOf(instance)); //true
        alert(SubType.prototype.isPrototypeOf(instance));   //true
        
        
    </script>
</head>
<body>

</body>
</html>

實現的本質是重寫原型對象,代之以一個新類型的實例。

1.別忘記默認的原型

全部函數的默認原型都是Object的實例

2.肯定原型和實例之間的關係

有兩種方法,第一種:instanof();第二種:isPrototypeOf()

3.謹慎的定義方法

給原型添加方法的代碼,必定要放在替換原型的語句以後。

4.原型鏈的問題

最主要的問題來自於包含引用類型值的原型。其次,在建立類型的實例時,不能向超類型的構造函數中傳遞參數,實際上沒有辦法在不影響全部對象實例的狀況下,經超類型的構造函數傳遞參數。所以,實踐中不多單獨使用原型鏈。

6.3.2 借用構造函數

爲解決原型中包含引用類型值出現的問題,開發人員開始使用一種借用構造函數的技術(有時候也叫作僞造對象或經典繼承)。這種技術的基本思想至關簡單,就是在了類型的構造函數裏調用超類型的構造函數。別忘了,函數不過是在特定環境裏執行代碼的對象,所以經過apply()、call()也能夠在新建立的對象上執行構造函數。

<!DOCTYPE html>
<html>
<head>
    <title>Constructor Stealing Example</title>
    <script type="text/javascript">
                    
        function SuperType(){
            this.colors = ["red", "blue", "green"];
        }

        function SubType(){  
            //inherit from SuperType
            SuperType.call(this);
        }

        var instance1 = new SubType();
        instance1.colors.push("black");
        alert(instance1.colors);    //"red,blue,green,black"
        
        var instance2 = new SubType();
        alert(instance2.colors);    //"red,blue,green"
        
        
    </script>
</head>
<body>

</body>
</html>

1.傳遞參數

<!DOCTYPE html>
<html>
<head>
    <title>Constructor Stealing Example</title>
    <script type="text/javascript">
                    
        function SuperType(name){
            this.name = name;
        }

        function SubType(){  
            //inherit from SuperType passing in an argument
            SuperType.call(this, "Nicholas");
            
            //instance property
            this.age = 29;
        }

        var instance = new SubType();
        alert(instance.name);    //"Nicholas";
        alert(instance.age);     //29
       
        
    </script>
</head>
<body>

</body>
</html>

2.問題

若是隻僅僅是借用構造函數,也沒法避免構造函數存在的問題----方法都在構造函數中定義,方法複用就無從談起。

6.3.2 組合繼承

有時候也叫僞經典繼承,指的是將原型鏈和借用構造函數組合到一塊兒使用,從而發揮兩者之長的一種繼承模式。其背後的思想是經過原型鏈實現對原型屬性和方法的繼承,經過過借用構造函數實現對實例屬性的繼承。這樣經過在函數上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性。

 

<!DOCTYPE html>
<html>
<head>
    <title>Combination Inheritance Example</title>
    <script type="text/javascript">
                    
        function SuperType(name){
            this.name = name;
            this.colors = ["red", "blue", "green"];
        }
        
        SuperType.prototype.sayName = function(){
            alert(this.name);
        };

        function SubType(name, age){  
            SuperType.call(this, name);
            
            this.age = age;
        }

        SubType.prototype = new SuperType();
        
        SubType.prototype.sayAge = function(){
            alert(this.age);
        };
        
        var instance1 = new SubType("Nicholas", 29);
        instance1.colors.push("black");
        alert(instance1.colors);  //"red,blue,green,black"
        instance1.sayName();      //"Nicholas";
        instance1.sayAge();       //29
        
       
        var instance2 = new SubType("Greg", 27);
        alert(instance2.colors);  //"red,blue,green"
        instance2.sayName();      //"Greg";
        instance2.sayAge();       //27
       
        
    </script>
</head>
<body>

</body>
</html>

 1)問題

組合繼承最大的問題就是不管什麼狀況下,都會調用兩次超類型構造函數,一次是在建立子類型原型的時候,另外一次是在子類型構造函數內部。

組合繼承成爲了JavaScript中最經常使用的繼承模式。並且instanceof和isPrototypeOf也可以用於識別基於組合繼承建立的對象。

6.3.4 原型式繼承

藉助原型能夠基於已有的對象建立新對象,同時還沒必要建立自定義類型。

ECMAScript5經過新增Objec.create()方法規範化了原型繼承。接收兩個參數:一個用做新對象原型的對象,一個爲新對象定義額外屬性的對象。引用類型值的屬性始終都會共享。

6.3.5 寄生式繼承 
主要考慮對象而不是自定義類型和構造函數的狀況下,寄生模式也是一種有用的模式。

<!DOCTYPE html>
<html>
<head>
    <title>Constructor Stealing Example</title>
    <script type="text/javascript">
                    
        function createAnother(original){
            var clone = object(original);//經過調用函數建立一個新對象,使用object()函數不是必須的,任何可以返回新對象的函數都適用這個模式
            clone.sayHi=function(){//以某種方式加強這個對象
             alert('hi');
            }
            return clone;//返回這個對象
        }

       var person = {
            name: "Nicholas",
            friends: ["Shelby", "Court", "Van"]
        };
                           
        var anotherPerson = Object.create(person);
        
       anotherPerson.sayHi();  //"hi"
       
        
    </script>
</head>
<body>

</body>
</html>

 使用寄生式繼承來爲對象添加函數,會因爲不能做到函數複用而下降效率,這一點與構造函數模式相似。

6.2.3 寄生組合式繼承

經過構造函數繼承屬性,用混成模式繼承方法。

<!DOCTYPE html>
<html>
<head>
    <title>Parasitic Combination Inheritance Example</title>
    <script type="text/javascript">
            
        function object(o){
            function F(){}
            F.prototype = o;
            return new F();
        }
    
        function inheritPrototype(subType, superType){
            var prototype = object(superType.prototype);   //create object
            prototype.constructor = subType;               //augment object
            subType.prototype = prototype;                 //assign object
        }
                                
        function SuperType(name){
            this.name = name;
            this.colors = ["red", "blue", "green"];
        }
        
        SuperType.prototype.sayName = function(){
            alert(this.name);
        };

        function SubType(name, age){  
            SuperType.call(this, name);
            
            this.age = age;
        }

        inheritPrototype(SubType, SuperType);
        
        SubType.prototype.sayAge = function(){
            alert(this.age);
        };
        
        var instance1 = new SubType("Nicholas", 29);
        instance1.colors.push("black");
        alert(instance1.colors);  //"red,blue,green,black"
        instance1.sayName();      //"Nicholas";
        instance1.sayAge();       //29
        
       
        var instance2 = new SubType("Greg", 27);
        alert(instance2.colors);  //"red,blue,green"
        instance2.sayName();      //"Greg";
        instance2.sayAge();       //27
       
        
    </script>
</head>
<body>

</body>
</html>

這個例子的高效率體如今它只調用了一次SuperType構造函數,避免了在SuperType.prototype上面建立沒必要要的、多餘的屬性,與此同時,原型鏈還能保持不變,所以還可以正常使做instanceof、isPrototypeOf(),開發人員廣泛認爲寄生組合繼承是引用類型最理想的繼承範式。

6.4 小結

ECMAScript支持面向(OO)對象編程,且不使用類和接口。對象能夠在代碼執行過程當中建立和加強,所以具備動態性和非嚴格定義的實體。

相關文章
相關標籤/搜索