JS 继承的多种方法

一、原型链

学过java的同学应该都知道,继承是java的重要特点之一,许多面向对象的语言都支持两种继承方式:接口继承和实现继承,接口继承只继承方法签名,而实现继承则继承实际的方法,在js中,由于函数没有签名,因此支持实现继承,而实现继承主要是依靠原型链来实现的,那么,什么是原型链呢?

首先,我们先来回顾一下构造函数,原型和实例之间的关系。当我们创建一个构造函数时,构造函数会获得一个prototype属性,该属性是一个指针,指向一个原型对象,原型对象包含一个constructor属性,该属性也是一个指针,指向构造函数,而当我们创建构造函数的实例时,该实例其实会获得一个[[Prototype]]属性,指向原型对象。

function SubType() {}
var instance = new SubType();

比如上面的代码,其中,SubType是构造函数,SubType.prototype是原型对象,instance是实例,这三者的关系可以用下面的图表示。

三者的关系

而这个时候呢,如果我们让原型对象等于另一个构造函数的实例,此时的原型对象就会获得一个[[Prototype]]属性,该属性会指向另一个原型对象,如果另一个原型对象又是另一个构造函数的实例,这个原型对象又会获得一个[[Prototype]]属性,该属性又会指向另一个原型对象,如此层层递进,就构成了实例与原型的链条,这就是原型链。

我们再看下上面的例子,如果这个时候,我们让SubType.prototype是另一个构造函数的实例,此时会怎么样呢?

function SuperType() {}
function SubType() {}
SubType.prototype = new SuperType();
var instance = new SubType();

上面的代码中,我们先是让SubType继承了SuperType,接着创建出SubType的实例instance,因此,instance可以访问SubType和SuperType原型上的属性和方法,也就是实现了继承,继承关系我们可以用下面的图说明。

继承关系

最后,要提醒大家的是,所有引用类型默认都继承了Object,这个继承也是通过原型链实现的,因此,其实原型链的顶层就是Object的原型对象啦。

二、继承

上面我们弄清了原型链,接下来主要就介绍一些经常会用到的继承方法,具体要用哪一种,还是需要依情况而定的。

1. 原型链继承

最常见的继承方法就是使用原型链实现继承啦,也就是我们上面所介绍的,接下来,还是看一个实际的例子。

function SuperType() {
  this.property = true;
}
SuperType.prototype.getSuperValue = function() {
  return this.property;
}
function SubType() {
  ths.subproperty = true;
}
SubType.prototype = new SuperType();// 实现继承
SubType.prototype.getSubValue = function() {
  return this.subprototype;
}
var instance = new SubType();
console.log(instance.getSuperValue());// true

上面的例子中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型,这个新原型就是SuperType的实例,因此,新原型具有作为SuperType实例所拥有的全部实现和方法,并且指向SuperType的原型,因此,instance实例具有subproperty属性,SubType.prototype具有property属性,值为true,并且拥有getSubValue方法,而SuperType拥有getSuperValue方法。

当调用instance的getSuperValue()方法时,因此在instance实例上找不到该方法,就会顺着原型链先找到SubType.prototype,还是找不到该方法,继续顺着原型链找到SuperType.prototype,终于找到getSuperValue,就调用了该函数,而该函数返回property,该值的查找也是同样的道理,会在SubType.prototype中找到该属性,值为true,所以显示true。

存在的问题:通过原型链实现继承时,原型实际上会变成另一个类型实例,而原先的实例属性也会变成原型属性,如果该属性为引用类型时,所有的实例都会共享该属性,一个实例修改了该属性,其它实例也会发生变化,同时,在创建子类型时,我们也不能向超类型的构造函数中传递参数。

2. 借用构造函数

为了解决原型中包含引用类型值所带来的问题,开发人员开始使用借用构造函数的技术实现继承,该方法主要是通过apply()和call()方法,在子类型构造函数的内部调用超类型构造函数,从而解决该问题。

function SuperType() {
  this.colors = ["red","blue","green"]
}
function SubType() {
  SuperType.call(this);// 实现继承
}
var instance1 = new SubType();
var instance2  = new SubType();
instance2.colors.push("black");
console.log(instance1.colors);// red,blue,green
console.log(instance2.colors);// red,blue,green,black

在上面的例子中,如果我们使用原型链继承,那么instance1和instance2将会共享colors属性,因为colors属性存在于SubType.prototype中,而上面我们使用了借用构造函数继承,通过使用call()方法,我们实际上是在新创建的SubType实例的环境下调用了SuperType的构造函数,因此,colors属性是分别存在instance1和instance2实例中的,修改其中一个不会影响另一个。

使用这个方法,我们还可以在子类型构造函数中向超类型构造函数传递参数。

function SuperType(name) {
  this.name = name;
}
function SubType() {
  SuperType.call(this,"Nicholas");
  this.age = 29;
}
var instance = new SubType();
console.log(instance.name);// Nicholas
console.log(instance.age);// 29

优点:解决了原型链继承中引用类型的共享问题,同时可以在子类型构造函数中向超类型构造函数传递参数。 缺点:定义方法时,将会在每个实例上都会重新定义,不能实现函数的复用。

3. 组合继承

组合继承主要是将原型链和借用构造函数的技术组合到一块,从而发货两者之长的一种继承模式,主要是使用原型链实现对原型属性和方法的基础,通过借用构造函数实现对实例属性的基础,这样,可以通过在原型上定义方法实现函数的复用,又能够保证每个实例都有自己的属性。

function SuperType(name) {
  this.name = name;
  this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = 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);
}

var instance1 = new SubType("Nicholas", 29);
var instance2 =new SubType("Greg", 27);
instance1.colors.push("black");
console.log(instance1.colors); // red,blue,green,black
console.log(instance2.colors); // red,blue,green
instance1.sayName(); // Nicholas
instance2.sayName(); // 29
instance1.sayAge(); // Greg
instance2.sayAge(); // 27 

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,现在已经成为js中最常用的继承方法。

缺点:无论什么情况下,都会调用两次超类型构造函数,一次是在创建子类型的时候,另一次是在子类型构造函数内部,子类型最终会包含超类型对象的全部实例属性,但是需要在调用子类型构造函数时重写这些属性。

4. 原型式继承

原型式继承主要的借助原型可以基于已有的对象创建新的对象,基本思想就是创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

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

从上面的例子我们可以看出,如果我们想创建一个对象,让它继承另一个对象的话,就可以将要被继承的对象当做o传递到Object函数里面去,Object函数里面返回的将会是一个新的实例,并且这个实例继承了o对象。

其实,如果我们要使用原型式继承的话,可以直接通过Object.create()方法来实现,这个方法接收两个参数,第一个参数是用作新对象原型的对象,第二个参数是一个为新对象定义额外属性的对象,一般来说,第二个参数可以省略。

var person = {
  name: "Nicholas",
  friends: ["Shelby","Court","Van"]
}
var anotherPerson = Object.create(person, {
  name: {
    value: "Greg"
  }
});
console.log(anotherPerson.name); // Greg

上面的例子中,我们让anotherPerson继承了person,其中,friends作为引用类型,将会被所有继承该对象的对象所共享,而通过传入第二个参数,我们可以定义额外的属性,修改person中的原有信息。

缺点:原型式继承中包含引用类型的属性始终都会共享相应的值。

5. 寄生式继承

寄生式继承其实和我们前面说的创建对象方法中的寄生构造函数和工程模式很像,创建一个仅用于封装继承过程的函数,该函数在内部以某种方法来增强对象,最后再返回该对象。

function createAnother(original) {
  var clone = Object(original);      
  // 通过调用函数创建一个新对象
  clone.sayHi = function() {
    console.log("hi");
  }
  return clone;
}

我们其实可以把寄生式继承看做是传进去一个对象,然后对该对象进行一定的加工,也就是增加一些方法来增强该对象,然后再返回一个包含新方法的对象的一个过程。

var person = {
  name: "Nicholas",
  friends:["Shelby","Court","Van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi

从上面的代码中我们可以看出,原来person是没有包含任何方法的,而通过将person传进去createAnother方法中进行加工,返回的新对象就包含了一个新的方法。

缺点:不能实现函数的复用。

6. 寄生组合式继承

组合继承是js中最经常用到的一种继承方法,而我们前面也已经说了组合继承的缺点,组合继承需要调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部,子类型最终会包含超类型对象的全部实例属性,但是我们不得不在调用子类型构造函数时重写这些属性。

function SuperType(name) {
  this.name = name;
  this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = 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);
}

上面的代码中有两次调用了超类型构造函数,那两次调用会带来什么结果呢?结果就是在SubType.prototype和SubType的实例上都会创建name和colors属性,最后SubType的实例上的name和colors属性会屏蔽掉SubType.prototype上的name和colors属性。

寄生组合式继承就是可以解决上面这个问题,寄生组合式继承主要通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,其实就是不必为了指定子类型的原型而调用超类型的构造函数,只需要超类型原型的一个副本就可以了。

function inheritPrototype(subType,SuperType) {
  var prototype = Object(SuperType); // 创建对象
  prototype.constructor = subType; // 增强对象
  subType.prototype = prototype; // 指定对象
}

在上面的例子中,第一步创建了超类型原型的一个副本,第二步为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性,最后一步将副本也就是新对象赋值给子类型的原型,因此,我们可以用这个函数去替换前面说到为子类型原型赋值的语句。

function SuperType(name) {
  this.name = name;
  this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}
function SubType(name,age) {
  SuperType.call(this,name);
  this.age = age;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function() {
  console.log(this.age);
}

寄生组合式继承只调用了一次SuperType构造函数,避免了在SubType.prototype上面创建的不必要的,多余的属性,现在也是很多人使用这种方法实现继承啦。

7. es6中的继承

我们在前面创建对象中也提到了es6中可以使用Class来创建对象,而同样的道理,在es6中,也新增加了extends实现Class的继承,Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {}
class ColorPoint extends Point {}

上面这个例子中可以实现ColorPoint类继承Point类,这种简洁的语法确实比我们上面介绍的那些方法要简洁的好多呀。

但是呢,使用extends实现继承的时候,还是有几点需要注意的问题,子类在继承父类的时候,子类必须在constructor方法中调用super方法,否则新建实例时会报错,这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法,如果不调用super方法,子类就得不到this对象。

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

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的,正确的继承之后,我们就可以创建实例了。

let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint; // true
cp instanceof Point; // true

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券