1
头图
与传统的面向对象语言不同,JavaScript的继承主要是通过原型链和借用构造函数的方式实现。今天我们就来学习下在JavaScript中常见的四种继承实现方式,分别是:原型链继承、借用构造函数继承、组合继承以及Class类继承。

原型链继承

原型链继承的核心思想是通过将子类的原型设置为父类实例的对象来实现对属性和方法的继承。

实际案例1:

//父类构造函数
function SuperType(){
  this.color=['红','橙','黄'];
  this.name="大壮";
}
//子类构造函数
function SubType(){}

//将子类的原型指向父类的实例
SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.color.push("紫");
instance1.name ="大顺"
console.log(instance1.name);   // 大顺
console.log(instance1.color);// ['红','橙','黄',"紫"]

var instance2 = new SubType();
console.log(instance2.color);// ['红','橙','黄',"紫"]
console.log(instance2.name);// 大壮

这里,我们可以清晰的看到,由于原型属性中引用的类型会被实例共享,而基本基本数据类型不会被实例共享,所以导致我们修改了原型上的name属性不会影响其他实例,而当修改了引用实例(color)却作用到了其他实例,这肯定不是我们想要的。
原型属性还有一个问题,就是我们在实例化属性的时候我们不能进行传参,比如我们在实例化属性的时候想要给name传一个值,但由于子类的参数无法共享给父类,所以是无法做到的。原型链继承主要问题总结如下:

  1. 原型属性上的引用类型数据修改后会污染其他的实例;
  2. 实例化对象时无法传参。

借用构造函数继承

借用构造函数继承的核心思想是通过call()、apply()函数在将来实例化的对象上执行构造函数。

实际案例2:

// 父类构造函数
function SuperType1(){
  this.color =  ['red','orange','yellow'];
  this.name = "DaZhuang";
}
// 子类构造函数
function SubType1(){
  SuperType1.call(this);
}

let instance3  = new SubType1();
instance3.color.push("purple");
instance3.name = "DaShun";
console.log(instance3.color);    //["red", "orange", "yellow", "purple"]
console.log(instance3.name);     //   DaShun

let instance4 = new SubType1(); 
console.log(instance4.color);    //['red','orange','yellow']
console.log(instance4.name);   //  DaZhuang

我们可以清楚地看到,通过借用构造函数,我们可以在创建子类实例的时候执行SuperType()上写好的初始化代码,这样每个实例都有一个color的副本了,很好的处理了引用类型属性污染其他实例的问题。
但是这种借用构造函数方式的继承也存在问题,那就是属性全都在父类中定义因此无法进行函数的复用,而且在父类原型中定义的方法对子类也是不可见的,结果所有类型都只能使用构造函数模式。因此借用构造函数的方式也很少单独使用。

组合继承

组合继承是指将原型链继承和借用构造函数继承这两种方式结合起来,从而发挥二者各自的长处的一种继承模式。
其核心思路是通过原型链实现对原型属性和方法的继承,而借用构造函数实现对实例属性和方法的继承。

实际案例3:

//父类构造函数
function Super (name) {
  this.name = name;
  this.colors = ["金", "木", "水"];
  this.sayBigName = function () {
    console.log('父类的方法');
  }
}

//子类构造函数
function Sub (name) {
  //实现父类属性和方法的继承
  Super.call(this, name);
}

//实现实例属性和方法的继承
Sub.prototype = new Super();
//修复Sub构造函数原型构造函数的指向,从Super变更为Sub
Sub.prototype.constructor = Sub;
Sub.prototype.sayName = function () {
  console.log(this.name);
}

var sub1 = new Sub("大壮");
sub1.colors.push("火");
console.log(sub1.colors);    //["金", "木", "水", "火"]
sub1.sayName();             //大壮
sub1.sayBigName();        //父类的方法

var sub2 = new Sub("大顺");
console.log(sub2.colors);    //  ["金", "木", "水"]
sub2.sayName();     //大顺

这个案例比较复杂,读者需要细细品读。首先读者要明确哪些属性和方法是在父类中声明的(对应案例中Super里面的:name,colors,sayBigName()),这些属性和方法是怎么实例化的呢?是通过13行代码(借用构造函数)实例化的,通过call方法在Sub上下文执行了Super属性和方法的实例化,这样Sub就拿到了Super里面的属性和方法。但是做完这一步是完全不够的,因为我们在子类中也有一些方法需要子类继承,这些方法我们也希望它能在实例化的时候被实例对象共享,这时候就需要采用原型链继承模式(代码17行)。

我们通过将子类(Sub)的原型指向父类的实例,这样在我们在子类中定义的方法和属性就会被存放到这个实例化的父类对象里面。在后续实例化Sub子类创建实例过程中,实例对象就可以通过原型链继承的形式拿到子类中定义的方法(sub1._ proto_ 等于Sub.prototype)。这样我们就实现了实例对象对父类对象属性和方法的继承以及子类对象方法的继承。

细心的同学可能会发现,我们并没有通过原型链模式让实例对象继承子类的属性,其原因其实我们在原型链继承模式部分已经介绍,就是因为使用原型链继承引用类型数据的时候存在污染其他实例对象的问题,因此我们采用原型链模式仅继承子类的方法而不对子类的属性进行继承(属性可以放到父类构造函数中)。

ES6 Class继承

在es6中,引入了class函数,通过class语法我们可以很容易的实现继承
实际案例4:
//父类
class Super {
  getValue () {
    console.log(this.val)
  }
}
//子类
class Sub extends Super {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let sub = new Sub(1)
sub.getValue() // 1
sub instanceof Super // true

不羁的风
35 声望4 粉丝

天下事有难易乎? 为者,则难者亦易已;不为,则易者亦难矣!