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

第一种:原型链继承

function Parent() {
  this.name = 'parent';
  this.play = [1,2,3];
}
function Child() {
  this.type = 'child';
}
Child.prototype = new Parent();
console.log(new Child);
复制代码

虽然父类的方法和属性都能够访问,但其实有一个潜在的问题,我再举个例子来说明这个问题。

var s1 = new Child();
var s2 = new Child();
s1.play.push(4);
console.log(s1.play, s2.play)
复制代码

这段代码的结果如下

截屏2021-06-16 下午9.17.14.png

明明我只改变了 s1 的 play 属性,为什么 s2 也跟着变了呢?原因很简单,因为两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。

那么要解决这个问题的话,我们就得再看看其他的继承方式,下面我们看看能解决原型属性共享问题的第二种方法。

第二种:构造函数继承(借用call)

  function Parent() {
    this.name = 'parent';
  }
  Parent.prototype.getName = function(){
    return this.name;
  }
  function Child() {
    Parent.call(this);
    this.type = 'child';
  }
  let child = new Child();
  console.log(child);
  console.log(child.getName());
复制代码

执行结果:

截屏2021-06-16 下午9.23.11.png

可以看到最后打印的 child 在控制台显示,除了 Child 的属性 type 之外,也继承了 Parent 的属性 name。这样写的时候子类虽然能够拿到父类的属性值,解决了第一种继承方式的弊端,但问题是,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法。这种情况的控制台执行结果如下图所示。

截屏2021-06-16 下午9.24.37.png

因此,从上面的结果就可以看到构造函数实现继承的优缺点,它使父类的引用属性不会被共享,优化了第一种继承方式的弊端;但是随之而来的缺点也比较明显——只能继承父类的实例属性和方法,不能继承原型属性或者方法。

上面的两种继承方式各有优缺点,那么结合二者的优点,于是就产生了下面这种组合的继承方式。

第三种:组合继承(前两种组合)

 function Parent() {
   this.name='parent';
   this.play=[1,2,3];
 }
 Parent.prototype.getName = function() {
   return this.name;
 }
 
 function Child(){
   // 第二次调用Parent
   Parent.call(this);
   this.type = 'child';
 }
 // 第一次调用Parent
 Child.prototype = new Parent();
 Child.prototype.constructor = Child;
 
 var s1 = new Child();
 var s2 = new Child();
 s1.play.push(4);
 console.log(s1.play, s2.play); // 互不影响
 console.log(s1.getName()); // parent
 console.log(s2.getName()); // parent
复制代码

执行上面的代码,可以看到控制台的输出结果,之前方法一和方法二的问题都得以解决。

但是这里又增加了一个新问题:通过注释我们可以看到 Parent 执行了两次,第一次是改变Child 的 prototype 的时候,第二次是通过 call 方法调用 Parent 的时候,那么 Parent 多构造一次就多进行了一次性能开销,这是我们不愿看到的。

那么是否有更好的办法解决这个问题呢?请你再往下学习,下面的第六种继承方式可以更好地解决这里的问题。

上面介绍的更多是围绕着构造函数的方式,那么对于 JavaScript 的普通对象,怎么实现继承呢?

第四种:原型式继承

这里不得不提到的就是 ES5 里面的 Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。

我们通过一段代码,看看普通对象是怎么实现的继承。

  let parent = {
     name: parent,
     friends: ['p1', 'p2', 'p3'],
     getName: function() {
       return this.name;
     }
  }
  
  let person = Object.create(parent);
  person.name = 'tom';
  person.friends.push('jerry');
  
  let person2 = Object.create(parent);
  person2.friends.push('lucy');
 
  console.log(person.name);
  console.log(person.name === person.getName());
  console.log(person2.name);
  console.log(person.friends);
  console.log(person2.friends);
复制代码

从上面的代码中可以看到,通过 Object.create 这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承 getName 的方法,请看这段代码的执行结果。

tom

true

parent

[‘p1’, ‘p2’, ‘p3’, ‘jerry’, ‘lucy’]

[‘p1’, ‘p2’, ‘p3’, ‘jerry’, ‘lucy’]

第一个结果“tom”,比较容易理解,person 继承了 parent 的 name 属性,但是在这个基础上又进行了自定义。

第二个是继承过来的 getName 方法检查自己的 name 是否和属性里面的值一样,答案是 true。

第三个结果“parent”也比较容易理解,person2 继承了 parent 的 name 属性,没有进行覆盖,因此输出父对象的属性。

最后两个输出结果是一样的,讲到这里你应该可以联浅拷贝的知识点,关于引用数据类型“共享”的问题,其实 Object.create 方法是可以为一些对象实现浅拷贝的。

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

第五种:寄生式继承

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

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


  let parent = {
     name: parent,
     friends: ['p1', 'p2', 'p3'],
     getName: function() {
       return this.name;
     }
  }
  
  function clone(sup) {
    let clone = Object.create(sup);
    clone.getFriends = function() {
     return this.friends;
    }
    return clone;
  }
  
  let person = clone(parent);
  
  console.log(person.getName());   // parent
  console.log(person.getFriends()); // ['p1', 'p2', 'p3'];
  
复制代码

通过上面这段代码,我们可以看到 person 是通过寄生式继承生成的实例,它不仅仅有 getName 的方法,而且可以看到它最后也拥有了 getFriends 的方法.

从最后的输出结果中可以看到,person 通过 clone 的方法,增加了 getFriends 的方法,从而使 person 这个普通对象在继承过程中又增加了一个方法,这样的继承方式就是寄生式继承。

我在上面第三种组合继承方式中提到了一些弊端,即两次调用父类的构造函数造成浪费,下面要介绍的寄生组合继承就可以解决这个问题。

第六种:寄生组合式继承

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

  function clone(parent, child) {
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
  }
  
  function Parent() {
    this.name = 'parent';
    this.play = [1, 2, 3];
  }
  Parent.prototype.getName = function(){
   return this.name;
  }
  
  function Child() {
    Parent.call(this);
    this.friends = 'child'
  }
  
  clone(Parent, Child);
  
  Child.prototype.getFriends = function() {
   return this.friends;
  }
  
  let person = new Child();
  console.log(person);
  console.log(person.getName());    // parent
  console.log(person.getFriends())  // child
复制代码

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

Tips: 子类 构造函数里通过call 调用父类构造函数的属性和方法(子类的构造函数里就会复制一份父类构造函数的属性和方法)

Tips: 子类 通过将自己 构造函数的原型 指向 父类 构造函数的原型 继承父类原型的属性和方法(改进:子类 通过将自己 构造函数的原型 指向 父类 实例—-这样的话就调用了两次构造函数 一次call 一次new)

整体看下来,这六种继承方式中,寄生组合式继承是这六种里面最优的继承方式。另外,ES6 还提供了继承的关键字 extends

第七种 ES6 的 extends 关键字

利用 extends 如何直接实现继承

  class Person {
    constructor(name) {
    this.name = name
  }

  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}

class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    super(name)
    this.age = age
  }
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享