JavaScript高级程序设计——第6章:面向对象的程序设计

ECMA-262把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。可以把ECMAScript对象想象成散列表,无非就是一组名值对,其中值可以是数据或函数。每个对象都是基于一个引用类型定义的,可以是原生类型,也可以是自定义类型。

6.1 理解对象

6.1.1 属性类型

ECMA-262在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。ECMA-262定义这些特性是为了实现JavaScritpt引擎用的。因此在JavaScritpt中不能直接访问它们。为了表示特性是内部值,该规范把它们放在了两对括号中。例如[[Enumerable]]。

ECMAScript中有两种属性:数据属性、访问器属性。

6.1.2 定义多个属性 

6.1.3 读取属性的特性

6.2 创建对象

虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

6.2.1 工厂模式

考虑到在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)对象编程,且不使用类和接口。对象可以在代码执行过程中创建和增强,因此具有动态性和非严格定义的实体。

原文地址:https://www.cnblogs.com/SmileX/p/5726567.html