js实现继承的几种方式

150 阅读4分钟

继承的概念:

先定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等,由汽车这个类可以派生出“轿车”和“货车”两个类,那么可以在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱。这样轿车和货车就是不一样的,但是二者都属于汽车这个类,这样从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系。

继承可以使得子类别具有父类的各种方法和属性,比如上面的例子中“轿车” 和 “货车” 分别继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性。在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法。

JS 实现继承的几种方式

第一种:原型链继承:

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

function Parent1() {    
this.name = 'parent1';    
this.play = [1, 2, 3]  
}  
function Child1() {    
this.type = 'child2';  
}  
Child1.prototype = new Parent1();  
console.log(new Child1());

上面的代码看似没有问题,虽然父类的方法和属性都能够访问,但其实有一个潜在的问题,我再举个例子来说明这个问题。
var s1 = new Child1();
var s2 = new Child1();
s1.play.push(4);
console.log(s1.play, s2.play);
这段代码在控制台执行之后,可以看到结果如下:


明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。

第二种:构造函数继承(借助 call):

function Parent1(){   
 this.name = 'parent1';  
}  
Parent1.prototype.getName = function () {   
 return this.name; 
 }  
function Child1(){    
Parent1.call(this);    t
this.type = 'child1' 
 }  
let child = new Child1(); 
console.log(child);  // 没问题 
console.log(child.getName());  // 会报错

可以看到最后打印的 child 在控制台显示,除了 Child1 的属性 type 之外,也继承了 Parent1 的属性 name。这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。这种情况的控制台执行结果如下图所示。
因此,从上面的结果就可以看到构造函数实现继承的优缺点,它使父类的引用属性不会被共享,优化了第一种继承方式的弊端;但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法,不能继承原型属性或者方法。

第三种:组合继承(前两种组合)

这种方式结合了前两种继承方式的优缺点,结合起来的继承。缺点是:

多构造一次就多进行了一次性能开销

第四种:原型式继承

这里不得不提到的就是 ES5 里面的 Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。

第五种:寄生式继承

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。

虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。

第六种:寄生组合式继承

结合第四种中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式

总结:

整体看下来,这六种继承方式中,寄生组合式继承是这六种里面最优的继承方式。