第六章 面向对象的程序设计

本章内容:

面向对象的语言有一个标志,那就是它们都有的概念,而通过类可以创建任意多个具有相同属性和方法的对象。

ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值,对象或者函数。

6.1 理解对象

创建自定义对象的最简单方式就是创建一个Object的实例,然后再为它添加属性和方法。

几年后,对象字面量成为创建这种对象的首选模式。

6.1.1 属性类型

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

1.数据属性

数据属性包含一个数据值的位置

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。用得比较少

2.访问器属性

访问器属性不包含数据值;它们包含一对儿getter和setter函数,不过这两个函数都不是必需的。

6.1.2 定义多个属性

ES5定义了一个Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性

6.1.3 读取属性的特性

使用ES5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符(比如是否是可配置的,可写的等)。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。

JS中可以针对任何对象——包括DOM和BOM对象,使用这个方法。

6.2 创建对象

6.2.1 工厂模式

一种设计模式,抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)

6.2.2 构造函数模式

构造函数可用来创建特定类型的对象,与前一个模式相比,可以不显式地创建对象;可以直接将属性和方法赋给this对象;没有return 语句

按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他的OO语言,主要是为了区别于ECMAScript中的其他函数

要调用Person的新实例,必须使用new操作符。


创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正式构造函数模式胜过工厂模式的地方。

1.将构造函数当做函数:构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数只要通过new操作符来调用,那它就可以作为构造函数。代码里展示了三种调用方式。

2.构造函数的问题:每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。每个Person实例都包含一个不同的Function实例(以显示name属性)。可以通过把函数定义转移到构造函数外部来解决这个问题。

问题:在全局作用域中定义的函数实际上只能别某个对象调用,名不副实。定义很多个全局函数,我们这个自定义的引用类型就丝毫没有封装性可言了。

### 6.2.3 原型模式

原型属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法

与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。

1.理解原型对象

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索从对象实例本身开始

这正是多个对象实例共享原型所保存的属性和方法的基本原理。

不能通过对象实例重写原型中的值。不过,使用delete操作符可以完全删除实例属性。

```hasOwnProperty()```方法只在给定属性存在于对象实例中时,才会返回true

2.原型与in操作符

有两种方式使用in操作符:单独使用和在for-in循环中使用

3.更简单的原型语法

为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象

4.原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改该原型也照样如此。

看这两种情况:

```js
    var friend = new Person();
    Person.prototype.sayHi = function(){
        alert("hi");
    };
    friend.sayHi(); //"hi"(没有问题!)

    function Person(){
    }
    var friend = new Person();
    Person.prototype = {
        constructor: Person,
        name : "Nicholas",
        age : 29,
        job : "Software Engineer",
        sayName : function () {
            alert(this.name);
        }
    };
    friend.sayName();   //error

这是为什么呢?原来第一种是修改原型,第二种是重写了原型,调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。friend 指向的原型中不包含以该名字命名的属性

5.原生对象的原型

所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。 9 例如,在 Array.prototype 中可以找到 sort()方法,而在 String.prototype 中可以找到 substring()方法

6.原型对象的问题

对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子:

    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

而这个问题正是我们很少看到有人单独使用原型模式的原因所在。

6.2.4 组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,最大限度地节省了内存。

目前使用最广泛,认同度最高的一种创建自定义类型的方法

动态原型模式

可以通过 检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

使用动态原型模式时,不能使用对象字面量重写原型

6.2.6 寄生构造函数模式

使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实 是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊 数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式。

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属 性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此, 不能依赖 instanceof 操作符来确定对象类型。

6.2.7 稳妥构造函数模式

道格拉斯·克罗克福德(Douglas Crockford)发明了 JavaScript 中的稳妥对象(durable objects)这 个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。

6.3 继承

继承是OO语言中的一个最为人津津乐道的概念。许多 OO 语言都支持两种继承方式:接口继承实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名, 在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链 来实现的。

6.3.1 原型链

让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

1.别忘记默认的原型

所有函数的默认原型都是 Object 的实例,因此默认原 型都会包含一个内部指针,指向 Object.prototype

2.确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。
第一种方式是使用instanceof操作符

第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该 原型链所派生的实例的原型,因此 isPrototypeOf()方法也会返回 true

3.谨慎地定义方法

子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎 样,给原型添加方法的代码一定要放在替换原型的语句之后。

    function SuperType(){
        this.property = true;
    }
    SuperType.prototype.getSuperValue = function(){
        return this.property;
    };
    function SubType(){
        this.subproperty = false;
    }
    //继承了 SuperType
    SubType.prototype = new SuperType();
    //添加新方法
    SubType.prototype.getSubValue = function (){
        return this.subproperty;
    };
    //重写超类型中的方法 SubType.prototype.getSuperValue = function (){
        return false;
    };
    var instance = new SubType();
    alert(instance.getSuperValue());   //false

第二个方法 getSuperValue()是原型链中已经存在的一个方法,但重写这个方法将会屏蔽原来的 那个方法。换句话说,当通过 SubType的实例调用getSuperValue()时,调用的就是这个重新定义 的方法;但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法。

还有一点需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这 样做就会重写原型链

由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断

4.原型链的问题

最主要的问题来自包含引用类型值的原型

当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此它也拥有了一个它自己的 colors属性——就跟专门创建了一个SubType.prototype.colors 属性一样。但结果是什么呢?结果是SubType的所有实例都会共享这一个 colors 属性。而我们对instance1.colors的修改能够通过instance2.colors反映出来,就已经充分证实了这一点。

6.3.2 借用构造函数

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。用到call,apply

1.传递参数

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函 数传递参数

2.借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。

6.3.3 组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的 技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继 承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

6.3.4 原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

    function object(o){
    	function F(){}
    	F.prototype = o;
    	return new F();
    }

object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的 原型,最后返回了这个临时类型的一个新实例。

6.3.5 寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该 函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

    function createAnother(original){ varclone=object(original); //通过调用函数创建一个新对象
    clone.sayHi = function(){//以某种方式来增强这个对象
        alert("hi");
    };
    return clone;//返回这个对象
    }

缺点:函数复用效率低

6.3.6 寄生组合式继承

前面说过,组合继承是 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);//第二次调用SuperType()
        this.age = age;
    }
    SubType.prototype = new SuperType();//第一次调用SuperType()
    SubType.prototype.constructor = SubType;
    SubType.prototype.sayAge = function(){
        alert(this.age);
    };

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背 后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型 原型的一个副本而已。

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

6.4 小结