浅谈JavaScript中常见的几种继承

继承方式

总所周知,在JavaScript中其实是利用原型和原型链来实现继承机制的,属性和方法要么存在于自身,要么存在于原型链上。那么很明显,我们实现继承时,无非就两种方法:要么将父类的属性和方法复制到子类,要么将子类和父类串到原型链上。

因为原型链的关系,JavaScript中的继承并不和传统OOP中的继承一样。在传统的OOP中,继承是一种复制行为,在JavaScript中,继承实际上是一种引用行为。

更多:简单说说原型和原型链

达成上面的共识之后,我们再来看看常见的这几种继承方式:

原型链继承

function SuperType() {
    this.name = 'Willem';
}
SuperType.prototype.sayHi = function() {
    console.log(this.name);
}

function SubType(age) {
    this.age = age;
}
// new返回的其实是一个__proto__指向SuperType.prototype和SuperType所有属性的对象
// 也就是实例化之后的 sub.__proto__指向了SuperType.prototype
SubType.prototype = new SuperType();

var sub = new SubType(18);
console.log(sub);

关系图如下:
在这里插入图片描述
这种继承的方式实际上是利用了new会创建一个拥有指向类的原型的__proto__,并复制类上所有属性的对象,直接将子类的prototype替换掉。上述图已经描述的很清楚了,不难理解,本质还是将子类和父类串在了原型链上,利用sub.__proto__.__proto__.__proto__...这种方式对被继承类的属性和方法进行访问。

缺点:

  • 引用类型会被共享。这点很明显,通过new SuperType()来手动修改SubType.prototype,相当于是直接将一个对象赋值给prototype,那么通过原型链找到的其实是同一个对象中的属性。
function Super() {
    this.colors = ['red', 'green', 'blue'];
}
function Child() {}
Child.prototype = new Super();

const c1 = new Child();
const c2 = new Child();
c1.colors.pop();
console.log(c1.colors, c2.colors); // ['red', 'green']  ['red', 'green']
  • 不能向父类传参

更多:模拟实现js中的new操作符

经典继承(借用构造函数)

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
    this.sayHi = function() {
        console.log(this.name);
    }
}

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

const sub = new SubType('Willem', 18);
const c2 = new SubType('Willem', 22);
sub.colors.pop();
console.log(sub.colors, c2.colors); // ["red", "green"] ['red', 'green', 'blue']

在这里插入图片描述
利用构造函数的方式,实际上是利用call函数将父类的所有属性和方法复制了一份到子类中,因为这种方式只是简单复制并没有修改原型链,所以只会继承父类构造函数中的属性,而不会继承父类prototype上的方法。

优点:

  • 避免了引用类型被共享。

缺点:

  • 因为方法都是定义在构造函数中,所以无法复用。
  • 无法使用instanceof判断sub和SuperType的关系。

更多:模拟实现Javascript中的call和apply

组合继承

组合继承是实际继承中使用最广泛的一种方式,将原型链继承和构造函数继承融合到了一起,集两家之长,及能保证属性的独立,又能复用原型上的函数。

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green'];
}
SuperType.prototype.sayHi = function() {
    console.log(this.name);
}

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

const c1 = new SubType('Willem', 18);
const c2 = new SubType('Wei', 20);
c1.colors.push('blue');
console.log(c1.colors, c2.colors); // ['red', 'green', 'blue'] ['red', 'green']
c1.sayHi(); // Willem
c1.sayAge(); // 18

原型式继承

这种继承方式其实就是Object.create的实现,对传入的对象进行了一个浅复制。利用这种方式也可以看做是修改了原型链的结构。

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

const person = {
    name: 'Willem',
    colors: ['red', 'green']
};

const p1 = createObject(person);
const p2 = createObject(person);
p1.name = 'Wei';
p1.colors.push('blue');
console.log(p1, p2);

在这里插入图片描述
缺点:

  • 可以看到的是,同样作为引用属性的colors被共享了。

寄生式继承

const person = {
    name: 'Willem',
    colors: ['red', 'green']
};

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

function createAnother(o) {
    const clone = createObject(o);
    clone.sayHi = function() {
        console.log(‘Hi’, this.name);
    }
    return clone;
}

createAnother(person)

利用工厂函数,所以能保证属性的独立性,但和借助构造函数一样,不能复用函数。

寄生组合式继承

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

function prototype(child, parent) {
    var prototype = createObject(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

prototype(Child, Parent);

小结

以上几种继承方式中,使用次数最多的便是组合继承了。而不管哪种继承方式,本质上无非就两种:复制属性、修改原型链。以上就是原型链的相关知识,希望对各位有所帮助。

参考:JavaScript高级程序设计第三版

阅读 239

推荐阅读