JavaScript七大继承解析

14 篇文章 0 订阅

原型链继承

首先我们简单回忆一下构造函数、原型、原型链之间的关系:每个构造函数有一个prototype 属性,它指向原型对象,而原型对象都有一个指向构造函数的指针 constructor,实例对象都包含指向原型对象的内部指针__proto__。如果我们让原型对象等于另一个构造函数的实例,那么此原型对象就会包含一个指向另一个原型的指针。这样一层一层,逐级向上,就形成了原型链。

根据上面的回顾,我们可以写出 原型链继承。

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['座椅', '轮子'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
}

Car.prototype.playMusic = function() {
  console.log('sing~');
};

// 将父构造函数的实例赋值给子构造函数的原型
Car.prototype = new Vehicle();

const car1 = new Car(4);

上面这个例子中,首先定义一个叫做 交通工具 的构造函数,它有两个属性分别是是 驱动方式 和 组成部分,还有一个原型方法是 跑;接下来定义叫做 汽车 的构造函数,它有 轮胎数量 属性和 播放音乐 方法。我们将 Vehicle 的实例赋值给 Car 的原型,并创建一个名叫 car1 的实例。

但该继承方式有几个缺点:

  • 多个实例对引用类型的操作会被篡改

  • 子类型的原型上的 constructor 属性被重写了

  • 给子类型原型添加属性和方法必须在替换原型之后

  • 创建子类型实例时无法向父类型的构造函数传参

  • 缺点 1:父类的实例属性被添加到了实例的原型中,当原型的属性为引用类型时,就会造成数据篡改。
    我们新增一个实例叫做 car2,并给car2.components 追加一个新元素。打印car1,发现 car1.components 也发生了变化。这就是所谓多个实例对引用类型的操作会被篡改。

const car2 = new Car(8);

car2.components.push('灯具');

car2.components; // ['座椅', '轮子', '灯具']
car1.components; // ['座椅', '轮子', '灯具']

  • 缺点 2 :原型链继承导致 Car.prototype.constructor被重写,它指向的是 Vehicle 而非 Car。因此你需要手动将 Car.prototype.constructor 指回Car
Car.prototype = new Vehicle();
Car.prototype.constructor === Vehicle; // true

// 重写 Car.prototype 中的 constructor 属性,指向自己的构造函数 Car
Car.prototype.constructor = Car;

  • 缺点 3:因为 Car.prototype = new Vehicle();重写了 Car 的原型对象,所以导致playMusic方法被覆盖掉了,因此给子类添加原型方法必须在替换原型之后。
function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
}

Car.prototype = new Vehicle();

// 给子类添加原型方法必须在替换原型之后
Car.prototype.playMusic = function() {
  console.log('sing~');
};

  • 缺点 4::显然,创建 car 实例时无法向父类的构造函数传参,也就是无法初始化powerSource属性。
const car = new Car(4);

// 只能创建实例之后再修改父类的属性
car.powerSource = '汽油';

借用构造函数继承

该方法又叫 伪造对象 或 经典继承。它的实质是 在创建子类实例时调用父类的构造函数。

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['座椅', '轮子'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;

  // 继承父类属性并且可以传参
  Vehicle.call(this, '汽油');
}

Car.prototype.playMusic = function() {
  console.log('sing~');
};

const car = new Car(4);

使用经典继承的好处是可以给父类传参,并且该方法不会重写子类的原型,故也不会损坏子类的原型方法。此外,由于每个实例都会将父类中的属性复制一份,所以也不会发生多个实例篡改引用类型的问题(因为父类的实例属性不在原型中了)。

然而缺点也是显而易见的,我们丝毫找不到run方法的影子,这是因为该方式只能继承父类的实例属性和方法,不能继承原型上的属性和方法。

为了将公有方法放到所有实例都能访问到的地方,我们一般将它们放到构造函数的原型中。而如果让 借用构造函数继承 运作下去,显然需要将 公有方法
写在构造函数里而非其原型,这在创建多个实例时势必造成浪费。

组合继承

组合继承吸收上面两种方式的优点,它使用原型链实现对原型方法的继承,并借用构造函数来实现对实例属性的继承。

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['座椅', '轮子'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
  Vehicle.call(this, '汽油'); // 第二次调用父类
}

Car.prototype = new Vehicle(); // 第一次调用父类

// 修正构造函数的指向
Car.prototype.constructor = Car;

Car.prototype.playMusic = function() {
  console.log('sing~');
};

const car = new Car(4);

虽然该方式能够成功继承到父类的属性和方法,但它却调用了两次父类。第一次调用父类的构造函数时,Car.prototype会得到 powerSourcecomponents两个属性;当调用Car构造函数生成实例时,又会调用一次 Vehicle 构造函数,此时会在这个实例上创建 powerSourcecomponents。根据原型链的规则,实例上的这两个属性会屏蔽原型链上的两个同名属性。

原型式继承

该方式通过借助原型,基于已有对象创建新的对象。

首先创建一个名为object 的函数,然后在里面中创建一个空的函数 F,并将该函数的 prototype指向传入的对象,最后返回该函数的实例。本质来讲,object()对传入的对象做了一次 浅拷贝。

function object(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

const cat = {
  name: 'Lolita',
  friends: ['Yancey', 'Sayaka', 'Mitsuha'],
  say() {
    console.log(this.name);
  },
};

const cat1 = object(cat);

虽然这种方式很简洁,但仍然有一些问题。因为 原型式继承 相当于 浅拷贝,所以会导致 引用类型 被多个实例篡改。下面这个例子中,我们给 cat1.friends 追加一个元素,却导致 cat.friends 被篡改了。

cat1.friends.push('Hachi');

cat.friends; // ['Yancey', 'Sayaka', 'Mitsuha', 'Hachi']

寄生式继承

该方式创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

const cat = {
  name: 'Lolita',
  friends: ['Yancey', 'Sayaka', 'Mitsuha'],
  say() {
    console.log(this.name);
  },
};

function createAnother(original) {
  const clone = Object.create(original); // 获取源对象的副本

  clone.gender = 'female';

  clone.fly = function() {
    // 增强这个对象
    console.log('I can fly.');
  };

  return clone; // 返回这个对象
}

const cat1 = createAnother(cat);

和 原型式继承 一样,该方式会导致 引用类型 被多个实例篡改,此外,fly 方法存在于 实例 而非 原型 中,因此 函数复用 无从谈起。

寄生组合式继承

上面我们谈到了 组合继承,它的缺点是会调用两次父类,因此父类的实例属性会在子类的实例和其原型上各自创建一份,这会导致实例属性屏蔽原型链上的同名属性。
好在我们有 寄生组合式继承,它本质上是通过 寄生式继承 来继承父类的原型,然后再将结果指定给子类的原型。这可以说是在 ES6 之前最好的继承方式了,面试写它没跑了。

function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // 创建父类原型的副本
  prototype.constructor = child; // 将副本的构造函数指向子类
  child.prototype = prototype; // 将该副本赋值给子类的原型
}

然后我们尝试写一个例子。

function Vehicle(powerSource) {
  this.powerSource = powerSource;
  this.components = ['座椅', '轮子'];
}

Vehicle.prototype.run = function() {
  console.log('running~');
};

function Car(wheelNumber) {
  this.wheelNumber = wheelNumber;
  Vehicle.call(this, '汽油');
}

inheritPrototype(Car, Vehicle);

Car.prototype.playMusic = function() {
  console.log('sing~');
};

它只调用了一次父类,因此避免了在子类的原型上创建多余的属性,并且原型链结构还能保持不变。

ES6 继承

功利主义来讲,在 ES6 新增 class 语法之后,上述几种方法已沦为面试专用。当然class 仅仅是一个语法糖,它的核心思想仍然是 寄生组合式继承,下面我们看一看怎样用 ES6 的语法实现一个继承。

class Vehicle {
  constructor(powerSource) {
    // 用 Object.assign() 会更加简洁
    Object.assign( this,{ powerSource, components: ['座椅', '轮子'] },
      // 当然你完全可以用传统的方式
      // this.powerSource = powerSource;
      // this.components = ['座椅', '轮子'];
    );
  }
  run() {
    console.log('running~');
  }
}

class Car extends Vehicle {
  constructor(powerSource, wheelNumber) {
    // 只有 super 方法才能调用父类实例
    super(powerSource, wheelNumber);
    this.wheelNumber = wheelNumber;
  }

  playMusic() {
    console.log('sing~');
  }
}

const car = new Car('核动力', 3);

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值