javascript实现继承的七种方式

6,843 阅读8分钟

继承是面向对象语言的基础概念,一般OO语言支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。ECMAScript中函数没有签名,因此无法实现接口继承。ECMAScript只支持实现继承,而其实现继承主要是靠原型链来实现。

一、原型链

原型链作为实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

利用原型链的思想实现继承的简单代码如下:

function Parent() {
    this.name = 'zhangsan';
    this.children = ['A', 'B'];
}

Parent.prototype.getChildren = function() {
    console.log(this.children);
}

function Child() {

}

Child.prototype = new Parent();

var child1 = new Child();
child1.children.push('child1')
console.log(child1.getChildren()); // Array ["A", "B", "child1"]

var child2 = new Child();
child2.children.push('child2')
console.log(child2.getChildren()); // Array ["A", "B", "child1", "child2"]

通过将子类的原型指向父类的一个实例实现继承,注意此时子类的constructor指向了父类

原型链继承的主要问题: 引用类型的属性被所有实例共享 在创建Child的实例的时候,不能向Parent传参

二、借用构造函数

为解决原型中包含引用类型值所带来的问题,人们开始用一种叫做借用构造函数的技术来实现继承。这种技术的基本思想非常简单,即在子类构造函数内部调用超类构造函数

function Parent(age) {
    this.names = ['lucy', 'dom'];
    this.age = age;

    this.getName = function() {
        return this.name;
    }

    this.getAge = function() {
        return this.age;
    }
}

function Child(age) {
    Parent.call(this, age);
}

var child1 = new Child(18);
child1.names.push('child1');
console.log(child1.names); // [ 'lucy', 'dom', 'child1' ]

var child2 = new Child(20);
child2.names.push('child2');
console.log(child2.names); // [ 'lucy', 'dom', 'child2' ]

优点:

  1. 避免了引用类型的属性被所有实例共享;
  2. 可以直接在Child中向Parent传参;

缺点:

  1. 方法都在构造函数中定义了,每次创建实例都会创建一遍方法,函数复用就无从谈起了

三、组合继承

组合继承就是将原型链和借用构造函数的技术结合到一起,发挥二者长处的一种继承模式,背后思想是使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。这样,既能够保证能够通过原型定义的方法实现函数复用,又能够保证每个实例有自己的属性。

function Parent(name, age) {
    this.name = name;
    this.age = age;
    this.colors = ['red', 'green']
    console.log('parent')
}

Parent.prototype.getColors = function() {
    console.log(this.colors);
}

function Child(name, age, grade) {
    Parent.call(this, name, age);// 创建子类实例时会执行一次
    this.grade = grade;
}

Child.prototype = new Parent(); // 指定子类原型会执行一次
Child.prototype.constructor = Child;// 校正构造函数
Child.prototype.getName = function() {
    console.log(this.name)
}

var c = new Child('alice', 10, 4)
console.log(c.getName())

> "parent"
> "parent"
> "alice"

组合继承既具有原型链继承能够复用函数的特性,又有借用构造函数方式能够保证每个子类实例能够拥有自己的属性以及向超类传参的特性,但组合继承也并不是完美实现继承的方式,因为这种方式在创建子类时会调用两次超类的构造函数。

四、原型式继承

这种方法并没有使用严格意义上的构造函数,思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

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

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上object()就是完成了一次浅复制操作

var person = {
    name: 'alice',
    friends: ['leyla', 'court', 'van']
}

var p1 = object(person);
p1.name = 'p1';
p1.friends.push('p1');

var p2 = object(person);
p2.name = 'p2';
p2.friends.push('p2');

console.log(p1.name)
console.log(person.friends) 

> Array ["leyla", "court", "van", "p1", "p2"]

ECMAScript5通过新增Object.create()方法规范化了原型式继承,这个方法接收两个参数:一个用作新对象原型的对象和为新对象定义属性的对象

var person = {
    name: 'alice',
    friends: ['leyla', 'court', 'van']
}

var p1 = Object.create(person);
p1.name = 'p1';
p1.friends.push('p1');

var p2 = Object.create(person);
p2.name = 'p2';
p2.friends.push('p2');

console.log(p1.name)
console.log(person.friends) 

> Array ["leyla", "court", "van", "p1", "p2"]

五、寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,即创建一个仅用于封装继承函数过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。

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

function createAnother(original) {
    var clone = object(original); // 创建新对象
    clone.sayHi = function(){ 
        console.log('hello, world'); // 增强对象,添加属性或方法
    }
    return clone; // 返回新对象
}

var person = {
    name: 'alice',
    friends: ['Sherly', 'Taissy', 'Vant']
}

var p1 = createAnother(person);
p1.sayHi(); 

> "hello, world"

新对象不仅具有 person 对象的属性和方法,还有自己的 sayHi() 方法 缺陷:使用寄生式继承来为对象添加函数,会由于不能做到函数复用造成效率降低,这一点与构造函数模式类似

六、寄生组合式继承

组合继承是 JavaScript最常用的继承模式,其最大的问题是不管在什么情况下都会调用两次超类构造函数:一次是在创建子类原型时,一次是在子类型构造函数内部。子类型最终会包含超类的全部实例属性。 所谓寄生组合式继承,即通过构造函数来继承属性,通过原型链继承方法,背后的基本思路是:不必为了指定子类的原型而调用超类的构造函数,我们所需要的无非就是超类原型的一个副本而已。寄生组合继承的基本模式如下所示:

// 复制父类的原型对象
function create(original) {
    function F(){};
    F.prototype = original;
    return new F();
}

// 创建父类的原型副本,改变子类的原型,同时纠正构造函数
function inherit(subClass, superClass) {
    var parent = create(superClass.prototype);
    parent.constructor = subClass;
    subClass.prototype = parent;
}

inherit() 函数实现了寄生组合式继承的最简单形式,参数有两个:子类构造函数和父类构造函数。在函数内部进行了三步操作:第一步是创建超类原型的一个副本,第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性,最后一步,将新创建的对象赋值给子类的原型。因此可以通过此函数去替换组合继承中为子类原型为赋值的语句了,即:

Child.prototype = new Parent();

完整的继承示例:

function Parent(name, age){
    this.name = name;
    this.age = age;
    console.log('parent')
}

Parent.prototype.getName = function(){
    return this.name;
}

function Child(name, age, grade){
    Parent.call(this, name, age);
    this.grade = grade;
}

// 寄生组合的方式
// 复制父类的原型对象
function create(original) {
    function F(){};
    F.prototype = original;
    return new F();
}

// 创建父类的原型副本,改变子类的原型,同时纠正构造函数
function inherit(subClass, superClass) {
    var parent = create(superClass.prototype);
    parent.constructor = subClass;
    subClass.prototype = parent;
}

inherit(Child, Parent);

var child = new Child('lucy', 12, 5);

> "parent"

寄生组合继承的高效率在于它只调用了一次超类构造函数,同时还能够保持原型链不变,能够正常使用 instanceof 和 isPrototypeOf() 寄生组合继承被普遍认为是引用类型最理想的继承方式

七、增强型寄生组合继承

寄生组合式继承能够很完美地实现继承,但也不是没有缺点。inherit() 方法中复制了父类的原型,赋给子类,假如子类原型上有自定的方法,也会被覆盖,因此可以通过Object.defineProperty的方式,将子类原型上定义的属性或方法添加到复制的原型对象上,如此,既可以保留子类的原型对象的完整性,又能够复制父类原型。

function Parent(name, age){
    this.name = name;
    this.age = age;
}

Parent.prototype.getName = function(){
    console.log(this.name)
}

function Child(name, age, grade){
    Parent.call(this, name, age);
    this.grade = grade;
}

// Child.prototype = new Parent();

// function inherit(child, parent){
//     let obj = parent.prototype;
//     obj.constructor = child;
//     child.prototype = obj;
// }

function inherit(child, parent){
    let obj = parent.prototype;
    obj.constructor = child;
    for(let key in child.prototype){
        Object.defineProperty(obj, key, {
            value: child.prototype[key]
        })
    }
    child.prototype = obj;
}

几种继承方式的比较:

继承方式优点缺陷
原型链继承能够实现函数复用1.引用类型的属性被所有实例共享;2.创建子类时不能向超类传参
借用构造函数1. 避免了引用类型的属性被所有实例共享; 2. 可以在子类中向超类传参方法都在构造函数中定义了,每次创建实例都会创建一遍方法,无法实现函数复用
组合继承融合了原型链继承和构造函数的优点,是Javascript中最常用的继承模式创建子类会调用两次超类的构造函数
原型继承在没有必要兴师动众地创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任引用类型的属性会被所有实例共享
寄生式继承可以增强对象使用寄生式继承来为对象添加函数,会由于不能做到函数复用造成效率降低,这一点与构造函数模式类似;同时存在引用类型的属性被所有实例共享的缺陷
寄生组合继承复制了超类原型的副本,而不必调用超类构造函数;既能够实现函数复用,又能避免引用类型实例被子类共享,同时创建子类只需要调用一次超类构造函数-