JavaScript 高级程序设计笔记(六)

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

面向对象的语言通常都有类的概念,可以通过类创建出很多对象,但 ECMAScript 没有类的概念,所以它的『对象』与有类的语言中的『对象』不同。

ECMA-262 把对象定义为:『无序属性的集合』---其属性可以包含基本值、对象或函数。对象是一组没有特定顺序的属性值,每个属性都对应一个名字

可以想象成散列表:无非就是一组键值对,其中值可以是数据或函数

6.1 理解对象

创建自定义对象最简单方式创建 Object 实例,然后添加属性和方法:

var person = new Object();  
person.name = "Nicholas";  
person.age = 29;  
person.job = "Software Engineer";  
person.sayName = function(){  
alert(this.name);  
};

之后流行用字面量来创建:

var person = {  
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",

    sayName: function(){
        alert(this.name);
    } 
};

6.1.1 属性类型

ECMAScript 中定义了一些实现 javascript 引擎的内部属性(不能直接访问),有两种属性:数据属性访问器属性

对象中的每个属性都有对应的内部属性(配置信息)

1. 数据属性
  • [[Configurable]] 能否通过 delete 删除,默认 true
  • [[Enumerable]] 可枚举,默认 true
  • [[Writable]] 能否修改,默认 true
  • [[Value]] 包含属性的数据值,默认 undefined

想要修改这些内部属性默认特性,就必须使用 ECMAScript 5 的 Object.defineProperty() 方法,该方法接收三个参数:

  • 属性所在的对象
  • 属性名
  • 描述符对象

例子 1:重新修改 writable 为 false,就不能再改名称了

var person = {};  
  Object.defineProperty(person, "name", {
      writable: false,
      value: "Nicholas"
  });

alert(person.name); //"Nicholas" person.name = "Greg"; alert(person.name); //"Nicholas"  

例子 2:重新修改 configurable 为 false 就不能对属性调用 delete 了

var person = {};  
Object.defineProperty(person, "name", {  
    configurable: false,
    value: "Nicholas"
});
alert(person.name); //"Nicholas" delete person.name; alert(person.name); //"Nicholas"  

例子 3:一旦 configurable 设为不可配置,除 writable 之外的属性都不能再修改了(一旦设定就不能修改回来了)

var person = {};  
Object.defineProperty(person, "name", {  
    configurable: false,
    value: "Nicholas"
});
// 抛出错误
Object.defineProperty(person, "name", {  
    configurable: true,
    value: "Nicholas"
});

不调用 Object.defineProperty()话这些特性默认都是 true,但调用之后不指定,configurable、enumerable 和 writable 特性默认值都是 false。

2. 访问器属性

访问器属性不含数据值,包含一对儿 getter 和 setter 函数(非必需),访问器属性有如下四个特性:

  • [[Configurable]]
  • [[Enumerable]]
  • [[Get]]:读取属性时调用的函数,默认值为 undefined
  • [[Set]]:写入属性时调用的函数,默认值为 undefined

访问器不能直接定义,必须使用 Object.defineProperty() 来定义

var book = {  
    _year: 2004,
    edition: 1 
};

Object.defineProperty(book, "year", {  
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        } 
    }
});

book.year = 2005;  
alert(book.edition); //2  

book 有两个属性,_yearedition_year 前面的下划线表示只能通过对象的访问器方法访问

6.1.2 定义多个属性

为了一次性的为某个对象多个属性进行内部属性的配置,ECMAScript 5 定义了一个 Object.defineProperties() 方法,可以通过描述符一次定义多个属性。

var book = {};

Object.defineProperties(book, {  
    _year: {
        writable: true,
        value: 2004
    },
    edition: {
        writable: true,
        value: 1 
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        } 
    }
});

6.1.3 读取属性的特性

使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。接受两个参数,返回值是一个对象:

var book = {};

Object.defineProperties(book, {  
    _year: {
        value: 2004
    },
    edition: {
        value: 1
},
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            } 
        }
    }
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");  
alert(descriptor.value); //2004  
alert(descriptor.configurable); //false  
alert(typeof descriptor.get); //"undefined"

var descriptor = Object.getOwnPropertyDescriptor(book, "year"); alert(descriptor.value); //undefined alert(descriptor.enumerable); //false  
alert(typeof descriptor.get); //"function"  

返回值是一个对象,它包含 book 对象关于 _year 成员的所有配置描述信息

本身是不能直接访问这些配置信息的,但我们可以使用 getOwnPropertyDescriptor() 方法将相关成员的配置描述信息提取出来,以键值对的形式放在对象中。

在 JavaScript 中,可以针对任何对象---包括 DOM 和 BOM 对象,使用 Object.getOwnPropertyDescriptor() 方法。

6.2 创建对象

使用 Object 构造函数或字面量创建多个对象,你要写很多重复代码,为了解决此问题,逐步采用下列方式

6.2.1 工厂模式

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");  

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

6.2.2 构造函数模式

ECMAScript 中的构造函数可用来创建特定类型的对象。Object 和 Array 这样的原生构造函数,在运行时会自动出现在执行环境中。

你也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

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");  

模仿 Object 和 Array 定义自己的构造函数,Person() 取代了 createPerson() 工厂方法,还有以下变化:

  • 没有显式创建对象
  • 直接将属性和方法赋给了 this 对象
  • 没有 return 语句

要创建 Person 的新实例,必须使用 new 操作符,这种方式调用构造函数,会经历以下四个步骤:

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象
  • 执行构造函数中的代码
  • 返回新对象

person1 和 person2 分别保存着 Person 的不同实例,这两个对象都有一个 constructor(构造函数)属性指向 Person,对象的 constructor 属性用来标识对象类型的,但检测对象类型还是用 instanceof 靠谱一些

构造函数就是用来创建某种类型的,创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;Person 可看做是自定义的(继承)Object(胜过工厂方式的地方)

1.将构造函数当做函数

构造函数和其他函数的唯一区别在于调用它们的方式不同。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;如果不通过 new 操作符来调用,就和普通函数一样了。

// 当做构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); //"Nicholas"

// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到 window  
window.sayName(); //"Greg"

// 在另一个对象的作用域中调用              
var o = new Object();  
Person.call(o, "Kristen", 25, "Nurse");  
o.sayName(); //"Kristen"  

不使用 new 操作符,属性和方法都添加到 window 对象上了,因为在全局作用域中调用一个函数时,this 对象总是指向 Global 对象

2.构造函数的问题

构造函数的问题是:每个方法在每个实例上重新创建一遍。person1 和 person2 的 sayName() 方法不是同一个 Function 实例,定义函数时等价于

function Person(name, age, job){  
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)"); // 与声明函数在逻辑上是等价的              
}

因为函数是对象,每定义一个函数,也就是实例化了一个对象

这种方式创建函数,会导致不同的作用域链和标识符解析。也就是说这种方式创建的不同实例,同名函数是不相等的,这点和其他语言不同。

6.2.3 原型模式

每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象。这个对象包含『由特定类型的所有实例共享的属性和方法』。

prototype 所指向的对象就是:通过调用构造函数而创建实例的原型对象,使用原型对象的好处在于可以让所有对象实例共享它所包含的属性和方法。

不必在构造函数中定义对象的实例的信息,而是将这些信息直接添加到原型对象中

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  

person1 和 person2 访问的都是同一组属性和方法

1.理解原型对象

只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,该属性指向函数的原型对象。

所有原型对象带一个 constructor 属性,又指回了构造函数,即 Person.prototype.constructor 指向 Person

创建自定义的构造函数后,它的原型对象默认得到 constructor 属性;

调用构造函数创建一个新实例后,该实例内部将包含一个指针,指向构造函数的原型对象;Firefox、Safari 和 Chrome 在每个对象上提供了一个属性支持访问 __proto__

实例内部的 [[Prototype]] 指向了构造函数的原型对象,与构造函数并没有关系

我们可以使用 isPrototypeOf() 来确定实例和原型对象之间的关系:

alert(Person.prototype.isPrototypeOf(person1)); //true  
alert(Person.prototype.isPrototypeOf(person2)); //true  

ECMAScript 5 允许使用 Object.getPrototypeOf() 来访问原型对象

alert(Object.getPrototypeOf(person1) == Person.prototype);  //true  
alert(Object.getPrototypeOf(person1).name);                 //"Nicholas"  

代码读取某个对象属性时,会首先从对象实例本身开始搜索,没有找到会继续去原型对象中搜索。实例对象也可以通过原型对象中的 constructor 得到自己的构造函数。

虽然可以通过对象实例访问原型中的值,但不能通过对象实例重写原型中的值。如果实例中的属性和原型中的重名,就屏蔽原型的。因为它首先搜索的是实例中的属性,找到了就不继续找了。

hasOwnProperty() 方法可以检测一个属性存在于实例中,还是存在于原型中。

2. 原型与 in 操作符

in 操作符有两种用法,除了 for-in 循环中使用外。单独使用时,in 操作符表示属性在实例中能否访问到,无论该属性存在于实例中还是原型中。

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();  
var person2 = new Person();

alert(person1.hasOwnProperty("name"));  //false  
alert("name" in person1);  //true

person1.name = "Greg";  
alert(person1.name); //"Greg" ---来自实例  
alert(person1.hasOwnProperty("name")); //true  
alert("name" in person1); //true

alert(person2.name); //"Nicholas" ---来自原型  
alert(person2.hasOwnProperty("name")); //false  
alert("name" in person2); //true  

一个属性不在原型中就在实例中,所以我们可以自己实现判断某个属性是否在原型中的方法

function hasPrototypeProperty(object, name){  
    return !object.hasOwnProperty(name) && (name in object);
}

在使用 for-in 循环时,返回的是能够通过对象访问的,可枚举的属性(既包括实例中的,也包括原型中的)。如果实例定义了一个属性,而这个属性在原型对象中也是存在的,这样原型中的属性会被屏蔽掉。所以即使原型中这个属性被定义为不可枚举,也不会影响用 for in 去枚举实例中的属性。

要取得对象上所有可枚举的属性,可以使用 Object.keys() 方法,接受的参数对象可以是原型也可以是实例对象

function Person(){  
}
Person.prototype.name = "Nicholas";  
Person.prototype.age = 29;  
Person.prototype.job = "Software Engineer";  
Person.prototype.sayName = function(){  
    alert(this.name);
};

var keys = Object.keys(Person.prototype);  
alert(keys);       //"name,age,job,sayName"

var p1 = new Person();  
p1.name = "Rob";  
p1.age = 31;  
var p1keys = Object.keys(p1);  
alert(p1keys);    //"name,age"  

要取得对象上所有属性(可枚举/不可枚举),使用 Object.getOwnPropertyNames() 方法

var keys = Object.getOwnPropertyNames(Person.prototype);  
alert(keys);    //"constructor,name,age,job,sayName"  

结果包含了不可枚举的 constructor 属性

3. 更简单的原型语法

之前每次添加一个属性都要写 Person.prototype = xxx,为了简便,也提供了字面量赋值

function Person(){  
}
Person.prototype = {  
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

我们将 Person.prototype 设置为一个字面量形式的新对象,但要注意此刻:constructor 属性不再指向构造函数 Person 了

前面说过,每创建一个函数,就会同时创建它的原型对象(prototype),这个原型对象也自带一个 constructor 属性。而上面的代码相当于重新为这个函数指定了一个新的原型对象,所以这个新原型上的 constructor 其实指向的是 Object 构造函数。

如果要恢复原型的 constructor 指向 Person,最好不要在字面量里直接写 constructor : Person

function Person(){  
}
Person.prototype = {  
    constructor : Person,  // 不要这样写
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};

这样虽然也能重设 constructor,但会导致它的 [[Enumerable]] 特性被设为 true。(默认是 false)

推荐使用 Object.defineProperty() 来修改

Object.defineProperty(Person.prototype, "constructor", {  
    enumerable: false,
    value: Person
});
4. 原型的动态性

所谓原型的动态性是指,任何时刻对原型进行修改,都会影响到相关实例

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  

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系。

5. 原型对象的原型

所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法

alert(typeof Array.prototype.sort);        // "function"  
alert(typeof String.prototype.substring);  // "function"  

我们也可以为已有的原生引用类型添加新方法(有点类似于 swift 的 extension)

String.prototype.startsWith = function (text) {  
    return this.indexOf(text) == 0;
};
var msg = "Hello world!";  
alert(msg.startsWith("Hello"));             //true  

但不推荐这么做,会导致命名冲突

6. 原型对象的问题

由于原型模式的共享本质,如果原型中包含引用类型属性,一个实例修改该属性会影响到所有的实例。

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

组合使用构造函数模式与原型模式:构造模式定义实例专有属性,而原型模式定义共享的方法和属性。

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,Count,Van"  
alert(person2.friends);    //"Shelby,Count"  
alert(person1.friends === person2.friends);  //false  
alert(person1.sayName === person2.sayName);  //true  

6.2.5 动态原型模式

所谓动态原型是指把所有信息都封装在了构造函数中,如果有必要,在构造函数内部初始化原型。比如可以检查某个方法是否存在,来决定是否需要初始化原型。

function Person(name, age, job){  
    // 属性
    this.name = name; 
    this.age = age; 
    this.job = job;
    // 方法 只有 sayName 不存在才会添加到原型中
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        }; 
    }
}

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

使用动态原型模式时,同样不能使用对象字面量重写原型,会切断现有实例与新原型之间的联系

6.2.6 寄生构造函数模式

这种和工厂模式没啥区别,只不过把最外层的包装方法叫做构造函数

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"  

事实上 friend 也并不是 Person 类型

6.2.7 稳妥构造函数模式

所谓稳妥对象,指没有公共属性,其方法也不引用 this 对象。适合安全环境中(禁用 this 和 new)。稳妥构造函数遵循于寄生构造函数类似的模式,有两点不同:

  1. 新创建的对象实例方法不引用 this
  2. 不使用 new 操作符调用构造函数
function Person(name, age, job){  
// 创建要返回的对象    
    var o = new Object();
// 可以在这里定义私有变量和函数

// 添加方法
o.sayName = function(){  
    alert(name);
};

// 返回对象
    return o; 
}

下面调用构造函数不使用 new,而且只能通过 sayName() 方法访问其数据成员 name

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

稳妥构造模式创建的对象与构造函数之间也没什么关系

6.3 继承

ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的

6.3.1 原型链

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例包含了指向原型对象的内部指针。

通常构造原型链是指将子类的原型指向父类的实例,这是实现原型链的基本模式

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;
};

var instance = new SubType();  
alert(instance.getSuperValue());    //true  

我们可以看到继承是通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype 实现的,实现的本质是重写原型对象,代之以一个新类型的实例。这样原来存在于父类 SuperType 实例中的所有属性和方法,现在也存在于 SubType.prototype 子类原型中

精髓在于:理解子类原型 SubType.prototype 指向 SuperType 的实例

父类 SuperType 的原型方法还在父类 SuperType 原型中,但父类 SuperType 的实例属性和方法已经算做是在子类 SubType 的原型中了

因为子类 SubType 实例的 constructor 会从 SubType.prototype 里找,但现在的 SubType.prototype 已被重写,因此会继续沿着 prototype 向上寻找。

所以子类 SubType 实例的 constructor 存放在 SuperType.prototype 中,即也就是指向了 SuperType。

调用 instance.getSuperValue() 会经历三个步骤:

  1. 搜索实例
  2. 搜索 SubType.prototype
  3. 搜索 SuperType.prototype
6.3.1.1 别忘默认原型

所有函数的默认原型都是 Object 的实例,因此所有函数都包含一个内部指针,指向 Object.prototype,这也是所有自定义的函数会继承 toString()valueOf() 等默认方法的原因

一句话:SubType 继承了 SuperType,而 SuperType 继承了 Object。当调用 instance.toString() 时,实际调用的是保存在 Object.prototype 中的那个方法

6.3.1.2 确定原型和实例关系
  1. 使用 instanceof 操作符,用来测试实例和原型链中的构造函数关系

    alert(instance instanceof Object);     //true
    alert(instance instanceof SuperType);  //true
    alert(instance instanceof SubType);    //true
    
  2. 使用 isPrototypeOf() 方法

    alert(Object.prototype.isPrototypeOf(instance));    //true
    alert(SuperType.prototype.isPrototypeOf(instance)); //true
    alert(SubType.prototype.isPrototypeOf(instance));   //true
    
6.3.1.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  

注意:往子类原型里添加新方法的时候不能使用字面量语法创建原型,这样会重写原型链

//继承了 SuperType
SubType.prototype = new SuperType();  
//使用字面量添加新方法,会导致上一行代码无效                      
SubType.prototype = {  
    getSubValue : function (){
        return this.subproperty;
    },
    someOtherMethod : function (){
        return false;
    } 
};
6.3.1.4 原型链的问题

最主要的问题来自于包含引用值的原型,包含引用类型值的原型属性会被所有实例共享;所以要在构造函数中,而不是原型对象中定义属性的原因。

而通过原型实现继承时,原型实际上会指向另一个类型的实例,所以原先实例的属性也变成了原型的属性,如果此时的属性中存在引用类型比如数组,这样子类实例修改这个属性会引起其他的子类实例也发生变化。

另一个问题是:在创建子类实例时,没法不影响所有对象实例的情况下,向超类的构造函数中传递参数。

所以,实践中很少单独使用原型链

6.3.2 借用构造函数

在子类的构造函数内部调用超类的构造函数。

function SuperType(){  
    this.colors = ["red", "blue", "green"];
}
function SubType(){  
    //继承了 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"

在新 SubType 对象上执行了 SuperType() 中定义的初始化代码,这样 SubType 每个实例都会有自己的 colors 属性的副本了。

这种模式可以在子类的构造函数中向父类构造函数传参,以满足自己的需要

function SuperType(name){  
    this.name = name;
}
function SubType(){  
//继承了 SuperType,同时还传递了参数          
SuperType.call(this, "Nicholas");  
//实例属性  
    this.age = 29;
}
var instance = new SubType();  
alert(instance.name);    //"Nicholas";  
alert(instance.age);     //29  

缺点是:方法都在构造函数中定义,无法复用

6.3.3 组合继承

原型链借用构造函数组合在一起,思想是使用原型链对原型属性和方法的继承,而通过构造函数实现对实例属性的继承。

function SuperType(name){  
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){  
    alert(this.name);
};
function SubType(name, age){  
    //继承属性,借用父类的构造方法,得到子类自己的 colors 和 name
    SuperType.call(this, name);
    this.age = age;
}
//继承方法  
SubType.prototype = new SuperType();  
//构造函数重新指回自己的,不然就变成 SuperType 了
SubType.prototype.constructor = SubType;  
//这一步向原型上添加方法
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  

组合模式融合了原型链和构造函数的优点,而 instanceof 和 isPrototypeOf() 也能正常识别基于组合继承创建的对象。成为最常用的继承模式。

6.3.4 原型式继承

其思想是借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型,为了达到目的要借助一个函数实现

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

object() 对传入的 o 进行了一次浅复制 ,说白了就是传一个实例进去给某构造函数做为原型

 var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
};
//anotherPerson 实例的原型是 person 实例
var anotherPerson = object(person);  
anotherPerson.name = "Greg";  
anotherPerson.friends.push("Rob");  
var yetAnotherPerson = object(person);  
yetAnotherPerson.name = "Linda";  
yetAnotherPerson.friends.push("Barbie");  
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"  

ECMAScript 5 新增了 Object.create() 方法替我们实现了 function object(o)

原型继承的缺点也很明显,引用类型值会在实例中共享。

6.3.5 寄生式继承

寄生式继承与原型式紧密相关,它的思路与寄生构造函数和工厂模式类似,创建一个仅用于封装继承过程的函数,在函数内部增强对象,最后再返回对象。

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

实际上就是把原型继承封装了一层

var person = {  
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);  
anotherPerson.sayHi(); //"hi"  

复用率比较低,也没解决引用类型值在实例中共享问题

6.3.6 寄生组合式继承

组合继承模式最大的缺点在于会调用两次超类型构造函数

  1. 第一次是创建子类原型时,需要初始化父类的实例,所以会调用父类构造函数
  2. 第二次在子类构造函数内部,明确地用 call 方法调用

下图展示了一次组合继承模式的过程

所谓寄生组合式继承:

  • 通过借用构造函数来继承属性
  • 通过原型链的混成形式来继承方法

背后的思路:不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无法就是超类原型的一个副本而已,毕竟就是得到超类的实例也是为了要超类的原型。

核心思想:使用寄生式继承来继承超类的原型,再将结果赋给子类型的原型

function inheritPrototype(subType, superType){  
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType;  // 增强对象
    subType.prototype = prototype; // 指定对象
}

下面替换掉 SubType.prototype = new SuperType();

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);
};

这里使用的 object(superType.prototype) 借助内部的 F() 避免调用 SuperType 的构造函数,代价是调用自己 F 的构造函数。

但这个互换的开销是值得的,避免二次调用 SuperType 的构造函数,可以不用在 SubType. prototype 上创建多余的属性,同时原型链还能保持不变,还能使用 instanceof 和 isPrototypeOf()。大家一致认为寄生组合式继承坠吼的


-EOF-

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!