探究 JS 常见的 6 种继承方式

478 阅读4分钟

1. 原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

function Parent() {
    this.name = 'parent';
    this.obj={age:1}
  }
  Parent.prototype.getName=function(){
     return this.name;
  }
  function Child() {
    this.type = 'child';
  }
  Child.prototype = new Parent(); 
  console.log(new Child());

这就是原型链继承的方式,其优点在于:父类的属性和方法都可以继承并且父类原型链上的方法和属性也能继承.

  var c1 = new Child();     
  var c2 = new Child();
  c1.obj.age = 2;
  console.log(c1.obj.age, c2.obj.age);//2,2
  

可以发现我们只修改了c1,c2也跟着变了,这就是原型链继承的缺点:因为两个实列共用一个原型对象,他们的内存空间也是共享的,所以当一个发生修改,其它的也会随之修改。

2. 构造函数继承

构造函数继承是借助call改变构造函数的this指向,让其执行的时候将this指向其实例,我们可以看下如下代码

function Parent(){
  this.name='Parent'
  this.obj={age:18}
}
Parent.prototype.getName=function(){
   return this.name
}
function Child(){
  Parent.call(this);
  this.age=18
}
 var child = new Child();
 console.log(child);  // {age:18,name:'Parent',obj: {age: 18}} 
 ---------------------------------------------------------------
 var c1 = new Child();
 var c2 = new Child();
 c1.obj.age=19;
 console.log(c1,c2) //c1改变,c2不变
 ---------------------------------------------------------------
 var c3=new Child();
 c3.getName() //报错 c3.getName是不一个函数

此时实例可以继承父类中的name和obj属性以及实例之间修改不会影响其他,成功解决了第一种继承方式的弊端,但是他的缺点也暴露出来,父类原型上的方法无法继承,那怎么让第一种继承和第二种继承融合在一起,取长补短一下,是不是就可以规避这2个问题呢,那接下来的第三种方法就实现了

3. 组合继承

听名字就知道是和组合起来的,顾名思义就是原型链继承和构造函数的组合起来实现的继承,代码如下:

function Parent(){
  this.name='parent';
  this.obj={age:18}
}
Parent.prototype.getName=function(){
   return this.name
}
function Child(){
  Parent.call(this)
  this.sex='男'
}
Child.prototype=new Parent();
 // 由于上面改变的prototype,所以需要手动挂上构造器,指向自己的构造函数
Child.prototype.constructor=Child

 var c1 = new Child();
 var c2 = new Child();
 c1.obj.age=19;
 console.log(c1.obj, c2.obj);  // 不互相影响
 console.log(c1.getName()); // 正常输出'parent'
 console.log(c2.getName()); // 正常输出'parent'

4. 原型式继承

我们知道Object.create这个方法是可以创建对象的,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。如下代码看看普通对象是怎么实现的继承。

 let person={
    name:'parent',
    obj:{age:1,sex:'男'},
    getName:function(){
      return this.name;
    }
 }
 let child1=Object.create(person);
 child1.name='child1'; 
 child1.obj.age=2; 
 let child2=Object.create(person);
 child2.obj.sex='女';
 
  console.log(childl.name); //child1
  console.log(childl.name === childl.getName()); //true
  console.log(child2.name);  //parent
  console.log(child1.obj);   //{age:2,sex:'女'}
  console.log(child2.obj);   //{age:2,sex:'女'}
  1. “child1”,比较容易理解,childl 继承了 parent 的 name 属性,但是在这个基础上又进行了自定义。
  2. 是继承过来的 getName 方法检查自己的 name 是否和属性里面的值一样,答案是 true。
  3. 第三个结果“parent”也比较容易理解,child2 继承了 parent 的 name 属性,没有进行覆盖,因此输出父对象的属性。
  4. 最后两个输出结果是一样的,因为 Object.create 方法是浅拷贝的。

那么关于这种继承方式的缺点也很明显,多个实例的引用类型属性指向相同的内存,存在篡改的可能,接下来我们看一下在这个继承基础上进行优化之后的另一种继承方式——寄生式继承。

5. 寄生式继承

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承。

虽然其优缺点和原型式继承一样,但是对于普通对象的继承方式来说,寄生式继承相比于原型式继承,还是在父类基础上添加了更多的方法。那么我们看一下代码是怎么实现。

   let parent = {
    name: "parent",
    arr: [1, 2, 3],
    getName: function() {
      return this.name;
    }
  };

  function clone(original) {
    let clone = Object.create(original);
    clone.getAge = function() {
      return this.age;
    };
    return clone;
  }

  let child = clone(parent); 
  console.log(child.getName());//parent
  console.log(child.getAge());//arr[1,2,3]

6. 寄生组合式继承

结合第四种中提及的继承方式,解决普通对象的继承问题的 Object.create 方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式,代码如下。

 function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
  }

  function Parent() {
    this.name = 'parent';
    this.arr = [1, 2, 3];
  }
   Parent.prototype.getName = function () {
    return this.name;
  }
  function Child() {
    Parent6.call(this);
    this.age = 18;
  }

  clone(Parent, Child);

  Child.prototype.getAge = function () {
    return this.age;
  }

  let child = new Child();
  console.log(child);
  console.log(child.getName());
  console.log(child.getAge());

通过这段代码可以看出来,这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销.

以上就是js的6种继承方式前5种各有优缺点,第6种相对完美。另外ES6还有class类种的extends方式实现继承,采用的就是寄生组合继承方式来实现。