0%

JavaScript 编程:5. 面向对象的程序设计

面对对象的程序设计

理解对象

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

  • 我们可以把 ECMAScript 的对象想象成散列表 / 字典:无非就是一组名值对,其中值可以是数据或函数。
  • 每个对象都是基于一个引用类型创建的。

属性类型

  • ECMA-262 第 5 版在定义只有内部才用的特性 (attribute) 时,描述了属性 (property) 的各种特征。
  • ECMA-262 定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它。
  • ECMAScript 中有两种属性:数据属性访问器属性

数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。

  • **[[Configurable]]**,能否删除、修改属性,默认值 true。
  • **[[Enumerable]]**,能否通过 for-in 枚举属性,默认值 true。
  • **[[Writable]]**,能否修改属性的值,默认值 true。
  • **[[Value]]**,属性的数据值,默认值 undefined。

要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符 (descriptor) 对象的属性必须是:configurableenumerablewritablevalue。设置其中的一或多个值,可以修改对应的特性值。

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

alert(person.name); //Nicholas
person.name = "Greg";
alert(person.name); //Nicholas
  • 一旦把属性定义为不可配置的(configurable = false),就不能再把它变回可配置了。也就是说,可以多次调用 Object.defineProperty() 方法修改同一个属性,但在把 configurable 特性设置为 false 之后就会有限制了。
  • 在调用 Object.defineProperty() 方法时,如果不指定,configurableenumerablewritable 特性的默认值都是 false

访问器属性

  • 访问器属性不包含数据值;它们包含一对 gettersetter 函数

访问器属性有如下 4 个特性:

  • **[[Configurable]]**,能否删除、修改属性,默认为 true。
  • **[[Enumerable]]**,能否通过 for-in 循环返回属性,默认为 true。
  • **[[Get]]**,在读取属性时调用的函数,默认值为 undefined。
  • **[[Set]]**,在写入属性时调用的函数,默认值为 undefined。

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

var book = {
    _year: 2018, // _表示只能通过对象方法访问的属性
    edition: 1
};

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

book.year = 2020;
console.log(book.edition); // 3

定义多个属性

Object.defineProperties() 方法可以同时为对象定义多个属性。

读取属性的特性

使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。

var book = {};

// 同时定义多个属性
Object.defineProperties(book, {
    // 定义数据属性
    _year: {
        value: 2016
    },
    edition: {
        value: 1
    },
    // 定义访问器属性
    year: {
        get: function () {
            return this._year;
        },
        set: function () {
            if (newValue > 2018) {
                this._year = newValue;
                this.edition += newValue - 2018;
            }
        }
    }
});

// 获取数据属性
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value); // 2016
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // undefined

// 获取访问器属性
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // function

创建对象

创建对象的两种方式:

// 1. 构造函数模式
var person = new Object();
person.name = "Tom";
person.age = 29;
person.job = "Software Engineer";

person.sayName = function() {
    alert(this.name);
};


// 2. 字面量模式
var person = {
    name: "Tom",
    age : 29,
    job:"Software Engineer",

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

缺点:使用同 一个接口创建很多对象,会产生大量的重复代码。

工厂模式

函数来封装以特定接口创建对象的细节。

// 把创建对象的所有过程封装在一个函数中
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("Tom", 29, "Software Engineer");
var person2 = createPerson("Andy", 23, "Doctor");

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

构造函数模式

原生构造函数: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("Andy", 23, "Software Engineer");
var person2 = new Person("Bob", 35, "Army");

console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true

构造函数模式 & 工厂模式的区别:

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

💡 构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

使用构造函数的问题:每个方法都要在每个实例上重新创建一遍。ECMAScript 中的函数是对象,因此每定义一个
函数,也就是实例化了一个对象。因此,不同实例上的同名函数是不相等的

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

// 把函数定义转移到构造函数外部
// 所有对象都共享同一个全局的 sayName 函数
function sayName(){
    alert(this.name);
}

var person1 = new Person("Andy", 23, "Software Engineer");
var person2 = new Person("Bob", 35, "Army");

问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

原型模式

  • 每个函数都有一个 prototype (原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
  • 使用原型对象的好处:让所有对象实例共享它所包含的属性和方法
// 构造函数变成了空函数
function Person(){
}

// 将 sayName()方法和所有属性直接添加到了 Person 的 prototype 属性中。
Person.prototype.name = "andy";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();
person1.sayName(); // "andy"

var person2 = new Person();
person2.sayName(); // "andy"

alert(person1.sayName == person2.sayName); // true,所有实例共享同一个方法

// 用原型对象的 isPrototypeOf() 方法测试 person1 和 person2。
// 因为它们内部都有一个指向Person.prototype 的指针,因此都返回了 true。
alert(Person.prototype.isPrototypeOf(person1)); //Person 是不是 person1 实例的原型?true
alert(Person.prototype.isPrototypeOf(person2)); //true

// 在所有支持的实现中,getPrototypeOf() 方法返回 [[Prototype]] 的值。
alert(Object.getPrototypeOf(person1) == Person.prototype); // true
alert(Object.getPrototypeOf(person1).name); // andy

image

图中:

  • Person.prototype 指向了原型对象。

  • Person.prototype.constructor 又指回 Person 函数。


  • 只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。
  • 在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数) 属性,这个属性包含一个指向 prototype 属性所在函数的指针。
  • 当调用构造函数创建一个新实例后,该实例的内部将包含一个指针 (内部属性),指向构造函数的原型对象。person1 和 person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype;换句话说,它们与构造函数没有直接的关系。
  • 虽然在所有实现中都无法访问到 **[[Prototype]]**,但可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系。从本质上讲,如果 [[Prototype]] 指向调用 isPrototypeOf() 方法的对象 (Person.prototype),那么这个方法就返回 true 。
  • ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回 **[[Prototype]]** 的值。
  • 使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中(返回 true),还是存在于原型中(返回 false)。

原型与 in 操作符

  • 在单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
  • 只要 in 操作符返回 truehasOwnProperty() 返回 false,就可以确定属性是原型中的属性。
function Person(){
}

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

var person1 = new Person();
var person2 = new Person();

// hasOwnProperty():实例属性存在于原型中,返回false
console.log(person1.hasOwnProperty("name")); // false

// in:不判断存在于原型中还是实例中,只要有就返回true
console.log("name" in person1); // true

// 只要 in 操作符返回 true 而 hasOwnProperty() 返回 false,就可以确定属性是原型中的属性。
function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}
  • 要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.keys() 方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
  • 如果你想要得到所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames() 方法。
function Person(){
}

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

var keys = Object.keys(Person.prototype);
console.log(keys); //[ 'name', 'age', 'job', 'sayName' ]

var person1 = new Person();
person1.name = "Andy";
person1.age = 24;
var person1Key = Object.keys(person1);
console.log(person1Key); //[ 'name', 'age' ]

更简单的原型语法

function Person(){
}

// 本质上完全重写了默认的 prototype 对象
Person.prototype = {
    name : "Andy",
    age : 28,
    job : "Doctor",
    sayName : function () {
        alert(this.name);
    }
};

// constructor 属性不再指向 Person 了。每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。

// 解决方法:
function Person(){
}

Person.prototype = {
    constructor : Person, // 将它设置为适当的值
    name : "Andy",
    age : 28,
    job : "Doctor",
    sayName : function () {
        alert(this.name);
    }
};

原型的动态性

  • 实例中的指针仅指向原型,而不指向构造函数。

  • 对原型对象所做的任何修改都能够立即从实例上反映出来 —— 即使是先创建了实例后修改原型也照样如此。

  • 如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]] 指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

    <!–hexoPostRenderEscape:

    function Person() {

}
var friend = new Person();

Person.prototype = {
constructor: Person,
name: “Tom”,
age: 24,
job: “Software Engineer”,
sayName: function () {
console.log(this.name);
}
};

friend.sayName(); // TypeError: friend.sayName is not a function:hexoPostRenderEscape–>

重写整个原型对象

原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。

原型对象的问题

  1. 无法为构造函数传递初始化参数,默认情况下,所有实例的初始化属性值相同。
  2. 所有属性、方法均共享的问题。(多个实例共享同一个属性的问题!)

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

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

function Person(name, age, job) {
    // 构造函数中定义实例属性
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Andy", "Bob"];
}

// 所有实例共享属性和方法在原型中定义
Person.prototype = {
    constructor: Person,
    sayName: function () {
        alert(this.name);
    }
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Grey", 27, "Doctor");

person1.friends.push("Van");
console.log(person1.friends); //[ 'Andy', 'Bob', 'Van' ]
console.log(person2.friends); //[ 'Andy', 'Bob' ]
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true

这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。

动态原型模式

function Person(name, age, job) {

    // 属性
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Andy", "Bob"];

    // 方法
    if(typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

var friend = new Person("Andy", 24, "Doctor");
friend.sayName();

⚠️

使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

寄生构造函数模式

基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

function Person(name, age, job){
    var object = new Object();
    object.name = name;
    object.age = age;
    object.job = job;
    object.sayName = function(){
        alert(this.name);
    }
    return object;
}

var friend = new Person("Andy", 24, "Doctor");
friend.sayName();

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中 (这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序 (如 Mashup 程序) 改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:

  • 一是新创建对象的实例方法不引用 this
  • 二是不使用 new 操作符调用构造函数。
function Person(name, age, job){

    // 创建要返回的对象
    var object = new Object();

    // 定义私有变量和函数
    object.name = name;
    object.age = age;
    object.job = job;

    // 添加方法
    object.sayName = function(){
        alert(name);
    }
    // 返回对象
    return object;
}

var friend = Person("Andy", 24, "Doctor");
// 除了调用 sayName() 方法外,没有别的方式可以访问其数据成员。
friend.sayName();

继承

  • 许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。

  • 由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

原型链

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

构造函数、原型和实例的关系:

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

// SuperType
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function () {
    return this.property;
};

// SubType
function SubType() {
    this.subproperty = false;
}

// 继承了 SyperType
// 这里 SubType 重写了 prototype 属性, [[prototype]] 的 constructor 属性指向 SuperType。
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
    return this.subproperty;
}

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

image

instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。

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

确定原型和实例的关系

1. instanceof 操作符

只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true。

alert(instance instanceof Object); // true
alert(instance instanceof SuperType); // true
alert(instance instanceof SubType); // true
2. isPrototypeOf () 函数

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

alert(Object.prototype.isPrototypeOf(instance)); // true
alert(SuperType.prototype.isPrototypeOf(instance)); // true
alert(SubType.prototype.isPrototypeOf(instance)); // true

原型链的问题

  • 问题一:在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

    <!–hexoPostRenderEscape:

    // 包含引用类型值的原型属性会被所有实例共享

function SuperType(){
this.colors = [“red”,“green”,“blue”];
}

function SubType(){
}

// 继承了 SuperType
// 相当于 SubType.prototype 变成了 SuperType 的一个实例
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push(“black”);
console.log(instance1.colors); //[ ‘red’, ‘green’, ‘blue’, ‘black’ ]

var instance2 = new SubType();
console.log(instance2.colors); // [ ‘red’, ‘green’, ‘blue’, ‘black’ ]
:hexoPostRenderEscape–>

  • 问题二:在创建子类型的实例时,不能向超类型的构造函数中传递参数。

💡💡💡

实践中很少会单独使用原型链。

借用构造函数

基本思想:在子类型构造函数的内部调用超类型构造函数。

函数只不过是在特定环境中执行代码的对象

// SuperType
function SuperType() {
    this.colors = ["red", "blue", "green"];
}

// SubType
function SubType() {
    // 继承了 SuperTyoe
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); //[ 'red', 'blue', 'green', 'black' ]

var instance2 = new SubType();
console.log(instance2.colors); //[ 'red', 'blue', 'green' ]

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

// SuperType
function SuperType(name) {
    this.name = name;
}

// SubType
function SubType() {
    // 继承了 SuperType,同时还传递了参数
    SuperType.call(this, "Bob");

    // 实例属性
    this.age = 34;
}

var instance1 = new SubType();
console.log(instance1.name); // Bob
console.log(instance1.age); //34

借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题 —— 方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

组合继承

将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。

基本思想:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

// SuperType 组合使用构造函数模式和原型模式
function SuperType(name){
    this.name = name;
    this.colors = ["red","green","blue"];
}

SuperType.prototype.sayName = function (){
    console.log(this.name);
};

// SubType 组合继承(原型链+借用构造函数)
function SubType(name, age){
    // 通过借用构造函数,继承属性
    SuperType.call(this, name);

    this.age = age;
}

// 通过原型链,继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function (){
    console.log(this.age);
};

var instance1 = new SubType("Andy", 23);
instance1.colors.push("black"); 
console.log(instance1.colors); //[ 'red', 'green', 'blue', 'black' ]
instance1.sayName(); // Andy
instance1.sayAge(); //23

var instance2 = new SubType("Bob", 24);
console.log(instance2.colors); //[ 'red', 'green', 'blue' ]
instance2.sayName(); // Bob
instance2.sayAge(); //24

原型式继承

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

function object(o) {
    function F() {} // 先创建一个临时性的构造函数
    F.prototype = o; // 然后将传入的对象作为这个构造函数的原型
    return new F(); // 返回这个临时类型的新实例
}
// 从本质上来说,object() 对传入其中的对象执行了一次浅复制。

// 1.有一个对象可以作为另一个对象的基础。
var person = {
    name : "Andy",
    friends : ["Shelby", "Court", "Van"]
};

// 2.把 基础对象 person 传递给 object() 函数,然后再根据具体需求对得到的对象加以修改
var anotherPerson = object(person);
anotherPerson.name = "Grey";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
anotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
console.log(anotherPerson.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
console.log(yetAnotherPerson.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]

console.log(anotherPerson.name); //Grey
console.log(yetAnotherPerson.name); //linda

ECMAScript 5 通过新增 Object.create() 方法规范化了原型式继承。这个方法接收两个参数:

  • 一个用作新对象原型的对象和 (可选的);
  • 一个为新对象定义额外属性的对象;

在传入一个参数的情况下,Object.create()object() 方法的行为相同。

var person = {
    name : "Andy",
    friends : ["Shelby", "Court", "Van"]
};

// 接收一个参数
var anotherPerson = Object.create(person);
anotherPerson.name = "Grey";
anotherPerson.friends.push("Rob");

// 接收两个参数
// 以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
var anotherPerson = Object.create(person ,{
    name:{
        value: "Grey"
    }
});

寄生式继承

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

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

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

寄生组合式继承

组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数

  • 一次是在创建子类型原型的时候;

  • 另一次是在子类型构造函数内部。

没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

// SuperType 组合使用构造函数模式和原型模式
function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = function () {
    console.log(this.name);
};

// 当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。
// 于是,这两个属性就屏蔽了原型中的两个同名属性。

function SubType(name, age) {
    SuperType.call(this, name); // 第二次调用 SuperType()

    this.age = age;
}

// 在第一次调用 SuperType 构造函数时, SubType.prototype 会得到两个属性:name 和 colors;
// 它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。

SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

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

function inheritProtoptype(subType, superType) {
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType; // 增强对象
    subType.prototype = prototype; // 指定对象
}
  1. 创建超类型原型的一个副本;
  2. 为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。
  3. 将新创建的对象 (即副本) 赋值给子类型的原型;

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceofisPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

欢迎关注我的其它发布渠道