js 如何实现继承?

282 阅读4分钟

最基本的实现一个继承的方法

// 1. 定义一个超类;
function SuperClass(){}
// 2. 给这个超类的原型对象添加一个say方法;
SuperClass.prototype.say = function(){
    console.log('i am superClass !!!');
}
// 3. 定义一个子类;
function SubClass(){}
// 4. 将子类的原型对象赋值为超类的一个实例
SubClass.prototype = new SuperClass(); // 这个时候,子类的实例就拥有超类的say方法了
// 5. 创建一个子类实例试试
var sub = new SubClass();
// 6. 尝试调用say方法;
sub.say(); // i am superClass !!!

其实第4步中,写成这样:SubClass.prototype = SuperClass.prototype也是可以的。本质上都是修改了SubClass的原型对象。但是,如果写成这个,那么子类SubClass的实例只能继承到超类SuperClass原型对象上的属性以及方法,无法继承到超类SuperClass本身的属性或者方法了。

所以实现继承的基本思想就是修改其原型对象。

继承的核心思想其实就是这些,但是就和设计模式一样,针对不同的业务需求,我们会用不同的方式去实现,形成了一些特殊的技巧。

当我们实现继承的时候,有几点需要注意:

1. 给原型对象添加方法一定要在替换原型之后
SubClass.prototype = new SuperClass();
// 所有的尝试给 SubClass.prototype.*** = *** 添加方法或者属性都要在上一句后面。 
2. 使用对象字面量方式添加新方法会导致 SubClass.prototype = new SuperClass()无效

上述这种原型链方式的继承的弊端

如果超类中有一个引用类型的值,比如数组,那么该超类的所有实例都会共享该数组,某个实例对该数组进行修改后,会影响到所有实例。那么有没有什么解决办法呢,那就是利用构造函数。

function SuperClass(){
    this.colors = ['red', 'blue'];
}
// 然后和上例一样,实现一个SubClass,并将其prototype指向new SuperClass();
var sub1 = new SubClass();
var sub2 = new SubClass();
sub1.colors.push('yellow'); // colors['red', 'blue', 'yellow']
sub2.colors // colors['red', 'blue', 'yellow'] // sub1对colors的操作影响到了sub2

构造函数实现继承

function SuperClass(){
    this.colors = ['red', 'blue'];
}
function SubClass(){
    SuperClass.call(this);
}
var sub1 = new SubClass();
var sub2 = new SubClass();
sub1.colors.push('yellow'); // colors['red', 'blue', 'yellow']
sub2.colors // colors['red', 'blue']

我们通过在子类SubClass中调用超类SuperClass构造函数,实现了继承。同时利用call修改了超类SuperClassthis。这样,每一个通过子类SubClass生成的实例,都保存了一份自己的colors

看起来构造函数的方式很好的解决了子类型实例共享引用类型的问题,但是新的问题又出现了。我们无法继承到超类原型上的方法和属性。那么怎么办呢,那就是将上面原型链式继承和构造函数式融合起来

组合继承

function SuperClass(name){
    this.name = name;
    this.colors = ['red', 'blue'];
}
SuperClass.prototype.paint = function(prop){
    console.log(this[prop]);
}
function SubClass(){
    SuperClass.call(this);
}
SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass // 这里如果不重新处理一下,SubClass.prototype.constructor指向的是SuperClass
var sub1 = new SubClass();
sub1.colors.push('yellow');
sub1.paint('colors') // ['red', 'blue', 'yellow']
var sub2 = new SubClass();
sub2.paint('colors') // ['red', 'blue']

通过这种方式,既解决了原型链继承方式中实例共享引用类型的弊端,又解决了构造函数模式下无法继承原型上方法或属性的问题。可谓两全其美。事实上,这也是最常用的实现继承的方式。

但是,你以为这就是最完美的继承方式了吗,显然不是。

寄生组合式继承

这种模式到底是怎么回事呢?我们来跟着上面组合继承实现的思想再来一遍:

  1. 首先我们通过原型链方式实现了继承,正美滋滋,觉得继承真简单,突然发现有个严重问题,就是所有实例都共享了引用类型,这可怎么办,想啊想
  2. 来到了第二步,利用构造函数,通过修改构造函数的this,我们解决了这个问题,但是,还来不及美滋滋的时候,我们又发现了另一个问题,没办法继承超类原型上的东西,WTF,那怎么办呢?
  3. 我们来到第三步,将两种方式融合实现了一个看着已经很牛逼,很完美的方式了。

但是,现实告诉我们,还有更厉害的方式:

首先,我们和上面一样,先来一个原型式继承:

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

这个object相当于将o复制了一份。我们通过这个函数拿到了一个复制品,拥有和o一样的属性和方法,是不是和上面第一种继承一样。当然也有一样的问题,就是共享引用类型的问题。

那么怎么办呢?

function inheritPrototype(subClass, superClass){
    var o = object(superClass.prototype);
    o.constructor = subClass;
    subClass.prototype = o;
}
function SuperClass(){
    this.colors = ['red', 'blue'];
}
SuperClass.prototype.paint = function(){
    console.log(this.colors);
}
function SubClass(){
    SuperClass.call(this);
}
inheritPrototype(SubClass, SuperClass);
var sub1 = new SubClass();
var sub2 = new SubClass();
sub1.colors.push('yellow');
sub1.paint(); // ['red', 'blue', 'yellow']
sub2.paint(); // ['red', 'blue']

实际上inheritPrototype函数做的就是复制一份超类SuperClass的原型对象。然后将子类的原型赋值为复制出来的这份,而不是直接将子类的原型赋值为超类SuperClass的实例。

这种方式好在哪里呢,就是减少了一次SuperClass构造函数的调用。(红宝书上这么说的)。

ES6 实现继承的方式

ES6对继承的实现更优雅、直观。但内部原理其实是一样的。

class SuperClass {
  constructor(){
      this.colors = ['red', 'blue'];
  }
  paint(){
      console.log(this.colors);
  }
}
class SubClass extends SuperClass {
  constructor(){
      super();
  }
}
var sub1 = new SubClass();
var sub2 = new SubClass();
sub1.colors.push('yellow');
sub1.paint(); // ['red', 'blue', 'yellow']
sub2.paint(); // ['red', 'blue']