说说 JavaScript 如何实现对象继承

1 原型链

可以利用 JavaScript 原型,让一个引用类型继承另一个引用类型的属性和方法:

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

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

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

//子类继承父类
SubType.prototype = new SuperType();

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

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

实现的本质是重写原型对象,代之以一个新类型的实例。它们之间的关系:

在通过原型链实现继承的情况下,搜索会沿着原型链向上查找,直到原型链末端才会停止,上面例子的搜索顺序是:
1. 搜索实例。
2. 搜索 SubType.prototype。
3. 搜索 SuperType.prototype。

1.1 默认的原型

注意,所有的引用类型默认都是继承自 Object,而这个继承也是通过原型链来实现的。因此,上面的例子的完整关系图是:

1.2 确定原型和实例的关系

如果实例的类型在原型链中曾经出现过,那么使用 instanceof 操作符就会返回 true:

console.log(instance instanceof Object);//true
console.log(instance instanceof SuperType);//true
console.log(instance instanceof SubType);//true

同样,也可以使用 isPrototypeOf()来判断:

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

1.3 谨慎地定义方法

子类型有时候需要重新父类型的方法,或者添加父类型中不存在的方法,记住,给原型添加方法一定要放在替换原型的语句后:

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

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

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

//子类继承父类
SubType.prototype = new SuperType();

//添加新方法
SubType.prototype.getSubValue = function () {
    return this.subproperty;
};

//重写父类中的方法
SubType.prototype.getSuperValue= function () {
    return false;
};

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

通过原型链来实现继承时,不能使用对象字面量的方式创建原型方法,因为这会重写原型链:

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

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

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

//子类继承父类
SubType.prototype = new SuperType();

//使用对象字面量添加新方法,会重写原型链,这导致上一行代码无效
SubType.prototype={
    getSubValue: function () {
        return this.subproperty;
    },
    someOtherMethod: function () {
        return false;
    }
};


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

1.4 原型链的问题

包含引用类型值的原型中定义的属性会被所有实例所共享,而通过原型链来实现继承时,原型变成另一个类型的实例,所以原先的实例中定义的属性也就变成了现在的原型属性了:

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

function SubType() {

}

//继承父类型
SubType.prototype = new SuperType();

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","black"

第二个问题是:在创建子类型的实例时,不能向父类型的构造函数传递参数。

因为这些问题,所以在实践中很少单独使用原型链来实现继承。

2 借用构造函数

因为函数是在特定环境下执行代码的对象,因此可以通过 apply() 或 call() 方法在新创建的对象上执行构造函数,即在子类型构造函数的内部调用父类型的构造函数,这样每个子类型实例就会有专属于它们自己的父类型属性了:

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

function SubType(){
    //继承父类型
    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"

2.1 传递参数

借用构造函数方式最大的优点就是,可以在子类型的构造函数中向超类型构造函数传递参数:

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

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

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

var instance = new SubType();
console.log(instance.name);//deniro
console.log(instance.age);//29

2.2 借用构造函数的问题

它的问题就像之前说到的构造函数模式创建对象一样,方法都只能在构造函数中定义,就无法实现复用函数,因此借用构造函数模式也很少单独使用。

3 组合继承

使用原型链实现对原型属性和方法的继承,然后借用构造函数实现对实例属性的继承:

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

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

function SubType(name, age) {
    //继承父类型的属性
    SuperType.call(this, name);

    //定义自己的属性
    this.age = age;
}

//继承父类型的方法
SubType.prototype = new SuperType();

//定义自己的方法
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

var instance1 = new SubType("Deniro", 19);
instance1.colors.push("black");
console.log(instance1.colors);//"red","blue","green","black"
instance1.sayName();//Deniro
instance1.sayAge();//19


var instance2 = new SubType("Lily", 15);
console.log(instance2.colors);//"red","blue","green"
instance2.sayName();//Lily
instance2.sayAge();//15

组合继承方式融合了原型链和借用构造函数方式的优点,而且 instanceof 和 isPrototypeOf() 也能够识别组合继承方式创建的对象,因此是最常用的继承模式。

4 原型式继承

通过借助原型在基于已有对象上创建新的对象,这样还不必创建自定义类型。首先需要定义了这样一个函数:

function object(o) {
    //创建临时性的构造函数
    function F() {
    }

    F.prototype = o;//将传入的对象作为这个构造函数的原型
    return new F();//返回这个临时类型的一个新实例
}

var person = {
    name: "Deniro",
    friends: ["Lily", "Jack", "Bruce"]
};

var anotherPerson = object(person);
anotherPerson.name = "Nancy";
anotherPerson.friends.push("Rob");

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

console.log(person.friends);//"Lily","Jack","Bruce","Rob","Barbie"

ECMAScript 5 使用 Object.create() 规范化了原型式继承。

输入参数 是否必填
用作新对象的原型对象
为新对象定义额外属性的对象

在传入一个参数的情况下,Object.create() 与 之前定义的 object() 行为相同。

var person = {
    name: "Deniro",
    friends: ["Lily", "Jack", "Bruce"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Nancy";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);//"Lily","Jack","Bruce","Rob","Barbie"

Object.create() 的第二个参数与 Object.defineProperties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符来定义的,这样定的的属性会覆盖原型对象上的同名属性:

 var person = {
    name: "Deniro",
    friends: ["Lily", "Jack", "Bruce"]
};

var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
console.log(anotherPerson.name);//"Greg"

如果只想让一个对象与另一个对象保持相似,那么可以使用原型式继承。不过要记住,包含引用类型值的属性会被所有的子类型所共享。

5 寄生式继承

创建一个只用于封装继承过程的函数,在函数内部以某种方法来增强对象,最后再返回这个对象:

function object(o) {
    function F() {
    }

    F.prototype = o;
    return new F();
}

function createAnother(original) {
    var clone = object(original);//创建一个新对象
    clone.sayHi = function () {//增强这个对象
        console.log("hi");
    };
    return clone;//返回这个对象
}

var person = {
    name: "Deniro",
    friends: ["Lily", "Jack", "Bruce"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();//hi

在主要考虑对象的情况下,可以用这个模式。示例中的 object() 函数不是必须的,二和能够返回新对象的函数都适用于该模式。


注意: 适用寄生式继承来为对象添加函数,也会像构造函数模式一样,因为无法做到函数复用而降低效率。


6 寄生组合式继承

前面说的组合继承也有不足,那就是会调用两次超类型的构造函数:
* 创建子类型原型时
* 在子类型构造函数内部时

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

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

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

    this.age = age;
}

//继承方法
SubType.prototype = new SuperType();//第一次调用 SuperType()

SubType.prototype.sayAge = function () {
    console.log(this.age);
};

第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这又会在新对象上创建 name 和 colors 的属性,这两个属性就屏蔽了原型中的两个同名属性。

寄生组合式继承,是通过借用构造函数来继承属性,通过原型链来继承方法。基本模式是:

function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);//创建父类型原型的一个副本
    prototype.constructor = subType;//增强对象,添加 constructor 属性
    subType.prototype = prototype;//将这个副本赋值给子类型的原型
}
function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

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

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

inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

这种方式比组合继承高效,因为它只调用一次 SuperType 构造函数,而且还避免了在 SubType.prototype 上创建不必要的、多余的属性。还能正常使用 instanceof 和 isPrototypeOf,是不是很棒O(∩_∩)O~。

综合以上优点,寄生组合式继承被认为是引用类型最理想的继承范式。

你可能感兴趣的:(JavaScript)