Js继承

229 阅读5分钟

js的继承是实现继承,通过原型链来实现的。

1. 原型链的继承

​ Js 一开始并没有面向对象的概念,而想要实现继承,则要通过原型链。

​ 在学习原型链的时候,大家就会发现这个原型链和继承有异曲同工之妙。

​ 我们来实验一下,本文中的SuperType是父类,SubType是子类。

先举一个错误的继承示例。

function SuperType() {
  this.color = 'red'
}
function SubType() {}
// 继承 SuperType
SuperType.prototype.test = 'test'
SubType.prototype = SuperType.prototype
let instance1 = new SubType()
console.log(instance1.test) // test

表面上看我们将父类的原型赋值给了子类的原型,那样instance1这个子类的实例想要访问test,它会去寻找它本身是否有test属性,发现没有,就会去找他的原型对象上是否有test属性,由于我们将他的原型SubType.prototype修改为了SuperType.prototype,而在Super.prototype上有test属性,因此找到了test,打印出来了。

看上去好像已经能够继承父类的原型属性,方法了,但其实不然。

function SuperType() {
  this.color = 'red'
}
function SubType() {}
// 继承 SuperType
SubType.prototype = SuperType.prototype
let instance1 = new SubType()
SubType.prototype.test = 'test'
let instance2 = new SuperType()
console.log(instance1.test) // test
console.log(instance2.test) // test

我们通过上述代码,发现在子类的原型上挂载属性,居然会影响父类实例。这是怎么回事呢?

其实原因是: 我们通过SubType.prototype = SuperType.prototype 是修改了子类原型的引用,直接将其指向了父类的原型。因此之后在子类的原型上修改,其实就是直接修改了父类原型。

既然这么继承不行,那么正确方式是什么呢?

function SuperType() {
  this.color = 'red'
}
function SubType() {}

SuperType.prototype.test = 'test'
// 继承 SuperType
SubType.prototype = new SuperType()
const instance1 = new SubType()
console.log(instance1.test) // test
console.log(instance1.color) // red

正确的方式是把父类的一个新的实例赋值给子类的原型。

instance1是子类的一个实例,它是如何找到test属性的?

继承.jpg

这样的继承就没有问题了?

function SuperType() {
  this.colors = ['red']
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType()
const instance1 = new SubType()
const instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ['red','black']
console.log(instance2.colors) // ['red','black']

我们知道一件事,那就是原型上的属性或者方法会共享给所有的实例。在这个例子当中,SubType.prototype 被修改成了一个实例,这个实例有自己的属性colors,虽然他本身是一个实例属性,但是在赋值给了SubType.prototype后这个属性变成了一个原型上的属性。 因此被共享给了所有的实例。也就有了上面的问题。

在开发中实现继承显然不能单独使用这个方法。

2.盗用构造函数继承

function SuperType() {
  this.colors = ['red']
}
function SubType() {
  // 继承 SuperType
  SuperType.call(this)
}

const instance1 = new SubType()
const instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ['red','black']
console.log(instance2.colors) // ['red']

这种继承方式很简单,就是在子类中执行父类的构造函数,那每一次创建子类的实例都会执行父类的构造函数,继承父类构造函数内的属性或方法。

相比较于原型链继承,好处是能够在子类传递参数给父类了,并且继承的属性不会成为实例上的共享变量。

但是缺点也很明显,那就是父类的原型上的方法无法被继承下来了。

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.sayAge = function () {
  console.log(this.age)
}
let instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors) // "red,blue,green,black"
instance1.sayName() // "Nicholas";
instance1.sayAge() // 29
let instance2 = new SubType('Greg', 27)
console.log(instance2.colors) // "red,blue,green"
instance2.sayName() // "Greg";
instance2.sayAge() // 27

这就是组合继承,结合了前两种继承方式,组合式继承确实能够完美的继承父类的实例以及原型。但是也有自身的缺点。

划重点:效率问题,父类构造函数始终会被调用两次,一次是在子类的构造函数里,一次是在修改子类原型时。

4. 原型式继承

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

原型式继承其实就是ES5 Object.create的一个模拟实现。

其实就是将传入的对象进行了一次浅复制。

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

let person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van'],
}

let anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')
let yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Barbie')
console.log(person.friends) // "Shelby,Court,Van,Rob,Barbie"
console.log(yetAnotherPerson.name) // Linda
console.log(person.name) // Nicholas

再这种情况下,person的实例如果是值类型就会属于各自的实例,而引用实例则是会共享。

用这种方式也可以某种程度上的实现继承,但是与其说是继承不如说是一种共享。

5. 寄生式继承

寄生式继承也是一种类似原型式继承的方式,背后的思路类似于寄生构造函数和工厂模式。

function createAnother(original) {
  let clone = object(original) // 通过调用函数创建一个新对象
  clone.sayHi = function () {
    // 以某种方式增强这个对象
    console.log('hi')
  }
  return clone // 返回这个对象
}

这种继承同样是不需要开发者自定义一个构造函数。

6. 寄生组合式继承

之前说了组合式继承会有一个效率问题,而前面介绍的两个原型继承,以及寄生式继承,其实就是为了引出寄生组合式继承,它能解决组合式继承的痛点。

寄生组合式继承是通过盗用构造函数来继承父类的属性,使用寄生式继承来继承原型链上的方法。

function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype) // 创建对象
  prototype.constructor = subType // 增强对象
  subType.prototype = prototype // 赋值对象
}

用了Object.create可以解决之前最一开始说的问题,不会在子类的原型上增添方法或属性影响到父类的原型,同时用这个方法还可以不用再调用构造函数了。

完整继承方案如下:

function inheritPrototype(subType, superType) {
  let prototype = Object.create(superType.prototype) // 创建对象
  prototype.constructor = subType // 增强对象
  subType.prototype = prototype // 赋值对象
}

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)
}