图解Javascript对象的继承

在前面《图解Javascript对象的创建》 一文中, 我们简单描述了JavaScript原型对象体系, 并简单介绍了原型链的相关概念。

和对象的创建一样, 开发者们根据自己的业务需求, 发明了形形色色的方法来模拟类的继承特性。 在JavaScript基于原型的对象体系中,对象之间的继承关系主要依靠原型链体系来实现。

本文将对一些主流的方法进行总结和梳理。

基于原型链的继承

JavaScript基于原型链实现继承关系的基本思想很简单:将子类构造函数的prototype属性指向一个父类的实例对象。 如下例,我们创建一个类型Cat继承了Animal

function Animal(name){
  this.name=name;
  this.favorFood=[];
};

Animal.prototype.sayHi= function (){
    console.log(`Hi, I'm ${this.name}`);
};

function Cat(){
}

// 继承语句
Cat.prototype = new Animal("animal name");

Cat.prototype.catchMouse = function(){
  console.log("catch a mouse..")
};

var cat1=new Cat();
var cat2=new Cat();

上面的例子里, 我们首先定义了Cat的构造函数,并将一个Animal的实例化对象作为它的原型对象; 随后又将一个catchMouse方法挂载到该原型对象上。

另外, 由于所有引用类型都继承于Object类型, 实质上也是由原型链实现的, 我们把整个链条串起来, 内存结构如下:

从图中可以看到, 我们在Animal中定义的属性namefavorFood在实例化对象new Animal的时候被创建, 而catchMouse整个方法则是被我们手动挂载在Cat.prototype上。

尽管我们在基类Animal中定义了属性name, 却无法在真正创建Cat对象的时候动态传参并赋值给属性。

同样的问题, 如果我们试图通过Cat的对象去修改的属性值favorFood:

cat1.favorFood.push("red fish");

cat2.favorFood; //["red fish"]

由于favorFood被挂载在Cat.prototype上, 任一实例对象对于它的修改都会影响到其他属性。

为了规避上面提到的问题, 我们需要在原型链的基础上对上述代码进行改进。

调用基类构造函数

首先, 从上图不难看出, 导致父类型属性被共享的原因在于相关属性被定义在了Cat的原型对象上了。 如果将这些属性放在子类型对象上是否可以解决问题呢? 我们对代码进行如下修改:











 











function Animal(name){
  this.name = name;
  this.favorFood = [];
};

Animal.prototype.sayHi= function (){
    console.log(`Hi, I'm ${this.name}`);
};

function Cat(name){
  Animal.call(this, name)
}
// 继承语句
Cat.prototype = new Animal("animal name");

Cat.prototype.catchMouse = function(){
  console.log("catch a mouse..")
};

var cat1=new Cat("cat1");
var cat2=new Cat("cat2");

我们在Cat的构造函数中调用了Animal函数, 这里的this指向的将要创建的对象。 同时, 还给Cat构造函数加上了入参, 这样就可以在创建对象的时候动态传参了。

Object.create

在上面的例子里, 由于不希望父类型属性被共享,我们在Cat构造函数中回调了Animal的构造函数, 保证namefavorFood属性被挂载到每个Cat的对象中。

同时,为了将子类型Catprototype属性设置为父类型的对象, 调用了父类型的构造函数:

// 继承语句
Cat.prototype = new Animal("animal name");

这两个操作导致在子类型的原型对象上Cat.prototype同样也挂载了namefavorFood属性, 而这完全没必要。 为了提高效率, 避免在子类型原型对象上创建不必要的属性, 可以利用Object.create函数进行如下优化:

function Animal(name){
  this.name = name;
  this.favorFood = [];
};

Animal.prototype.sayHi= function (){
    console.log(`Hi, I'm ${this.name}`);
};

function Cat(name){
  Animal.call(this, name)
}
// 继承语句
// Cat.prototype = new Animal("animal name");
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.catchMouse = function(){
  console.log("catch a mouse..")
};

var cat1=new Cat("cat1");
var cat2=new Cat("cat2");

ES5提供了方法 Object.create(proto, [propertiesObject]) 用于创建一个新对象,并将传入的参数proto作为新对象[[prototype]]属性的值, 通过入参propertiesObject可以定义新对象自身的属性。

利用Object.create, 我们基于Animal.prototypeCat创建了原型对象, 结构如下:

ES6 的 extend

在ES6之后引入了classextends关键字, 继承的操作就方便了很多:

class Animal{
  constructor(name){
    this.name=name;
  }
  sayHi(){
    console.log(`Hi, I'm ${this.name}`);
  }
}
class Cat extends Animal{
  constructor(name, age){
    super(name);
    this.age=age;
  }
}
let c1 = new Cat("c1", 2);
let c2 = new Cat("c2", 1);

尽管最终的运行机时制仍然是基于原型体系的, 但语法上清爽了很多。