JS的继承

110 阅读5分钟

概述

如果真的是一种简易的脚本语言,其实不需要有"继承"机制。但是,Javascript 里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich 最后还是设计了"继承"。

他想到 C++和 Java 使用 new 命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在 Javascript 语言中,new 命令后面跟的不是类,而是构造函数。

举例来说,现在有一个叫做 DOG 的构造函数,表示狗对象的原型。

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

对这个构造函数使用 new,就会生成一个狗对象的实例。

var dogA = new DOG('大毛');
alert(dogA.name); // 大毛

注意构造函数中的 this 关键字,它就代表了新创建的实例对象。

用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。 比如,在 DOG 对象的构造函数中,设置一个实例对象的共有属性 species。

function DOG(name){
  this.name = name;
  this.species = '犬科';
}

然后,生成两个实例对象:

var dogA = new DOG('大毛');
var dogB = new DOG('二毛');

dogA.species = '猫科';
alert(dogB.species); // 显示"犬科",不受dogA的影响

这两个对象的 species 属性是独立的,修改其中一个,不会影响到另一个。每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费。

prototype 实现继承

为了解决从原型对象生成实例的问题,Javascript 提供了一个构造函数(Constructor)模式。

所谓"构造函数",其实就是一个普通函数,但是内部使用了 this 变量。对构造函数使用 new 运算符,就能生成实例,并且 this 变量会绑定在实例对象上。

比如,猫的原型对象现在可以这样写,

function Cat(name,color){
  this.name=name;
  this.color=color;
}

我们现在就可以生成实例对象了。

var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");

alert(cat1.name); // 大毛
alert(cat1.color); // 黄色

但是我们有一个"动物"对象的构造函数。

function Animal(){
  this.species = "动物";
}

怎样才能使"猫"继承"动物"呢?

构造函数绑定

第一种方法也是最简单的方法,使用 call 或 apply 方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

function Cat(name,color){
  Animal.apply(this, arguments);
  this.name = name;
  this.color = color;
}

var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物

prototype 模式

第二种方法更常见,使用 prototype 属性。

如果"猫"的 prototype 对象,指向一个 Animal 的实例,那么所有"猫"的实例,就能继承 Animal 了。

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物

它相当于完全删除了 prototype 对象原先的值,然后赋予一个新值。并将 Cat.prototype 对象的 constructor 值改为 Cat。

直接继承 prototype

第三种方法是对第二种方法的改进。由于 Animal 对象中,不变的属性都可以直接写入 Animal.prototype。所以,我们也可以让 Cat()跳过 Animal(),直接继承 Animal.prototype。

function Animal(){ }

Animal.prototype.species = "动物";

然后,将 Cat 的 prototype 对象,然后指向 Animal 的 prototype 对象,这样就完成了继承。

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

var cat1 = new Cat("大毛","黄色");
alert(cat1.species); // 动物

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立 Animal 的实例了),比较省内存,并且通过“Object.create”方法创建了一个新的 Animal.prototype 对象让 cat 函数继承。

class 的继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

首先定义父类对象

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

上面代码中,constructor 方法和 toString 方法之中,都出现了 super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。

如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法。

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // 错误
    super(x, y);
    this.color = color; // 正确
  }
}

上面代码中,子类的 constructor 方法没有调用 super 之前,就使用 this 关键字,结果报错,而放在 super 方法之后就是正确的。

最后,父类的静态方法,也会被子类继承。

class A {
  static hello() {
    console.log('hello world');
  }
}

class B extends A {
}

B.hello()  // hello world