blog icon indicating copy to clipboard operation
blog copied to clipboard

JS常用继承方案

Open BlingSu opened this issue 4 years ago • 0 comments

当一听到继承的时候,最容易想到的就是 ES6 的 extends,但是如果要从函数和原型链的角度上实现继承的话应该怎么做呢?

原型链继承

原型链继承的原理其实很简单,就是直接让子类的原型对象指向父类实例,如果子类实例找不到对应的属性和方法的时候,就会往原型对象上面去找,也就是父类的实例上找,这样就实现了对父类的属性和方法的继承。

// 父类
function Parent () {
  this.name = 'parent name'
}
// 父类原型方法
Parent.prototype.getName = function () {
  return this.name
}
// 子类
function Child () {}
/**
 * 让子类的原型对象指向父类实例
 * 在Child实例中找不到的属性和方法就会在原型对象(父类实例)上去找
 * 可以理解称把原来的prototype删了重新赋值!
*/
Child.prototype = new Parent()
/**
 * 任何一个prototype对象都有一个constructor属性,指向它的构造函数
 * 如果没有 Child.prototype= New Child() 的话, Child.prototype.constructor 是指向 Parent的
 * 如果有这一行的话 Child.prototype.constructor 指向 Child
 * */ 
Child.prototype.constructor = Child

const child = new Child()
child.name // parent name
child.getName() // parent name

看起来不错,但是原型继承有几个缺点

  1. 因为所有的Child实例原型都是指向同一个Parent实例的,所以如果对某个Child实例的父类引用类型变量修改就会影响到所有的Child实例子
  2. 创建子类实例时候没有办法向父类构造函数传承,也就是没有实现 super()的功能
/**
 * 案例 
 * 比如我把上面的实例创建两个去修改其一另外一个也挂了
 * 重点:引用类型,所以这里的父类属性就得改成 this.name = ['parent name']
 * */ 
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'change'
console.log(child1.name[0], child2.name[0]) // change  change

构造函数继承

构造函数继承,就是在子类的构造函数中执行父类的构造函数,并切为其绑定子类的 this,让父类的构造函数把成员属性和方法都挂在子类的this上去,这样可以避免实例之间共享一个原型实例,又可以向父类构造方法传参,简单说就是将父对象的构造函数绑定在子对象上

function Parent (name) {
  this.name = [name]
}
Parent.prototype.getName = function () {
  return this.name
}
function Child () {
  // 执行父类构造方法并且绑定子类的this,让父类的属性可以赋到子类的this上
  Parent.call(this, 'parent name')
}

// 测试
const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'change'
console.log(child1.name, child2.name) // ['change'] ['parent name']
child2.getName() // 报错,找不到这个方法,构造函数继承的方式继承不到父类原型上的属性和方法
  • 核心代码是 Parent.call(this) ,创建子类实例时调用 Parent 构造函数,于是 Child 的每个实例都会将 Parent 中的属性复制一份

缺点

  1. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  2. 无法实现复用,每个子类都有父类实例函数的副本,影响性能

组合式继承

既然原型链继承和构造函数继承都有各自互补的优缺点,那么如果把他们组合起来,会不会比较好?这就是组合式继承,组合两种方式的继承方式

function Parent (name) {
  this.name = [name]
}
Parent.prototype.getName = function () {
  return this.name
}
function Child () {
  Parent.call(this, 'parent name')
}
Child.prototype = new Parent()
Child.prorotype.constructor = Child

const child1 = new Child()
const child2 = new Child()
child1.name[0] = 'change'
console.log(child1.name[0]) // change
console.log(child2.name[0]) // parent name
child2.getName() // ['parent name']

缺点 每次创建子类实例都执行了两次构造函数(Parent.call())和 (new Parent()),虽然不影响对父类的继承,但是子类创建实例的时候会存在两份相同的属性和方法,这样就显得很 Low

寄生组合继承

为了解决组合继承的父类构造函数执行两次的问题,我们可以将指向父类实例改为指向父类原型

function Parent (name) {
  this.name = [name]
}
Parent.prorotype.getName = function () {
  return this.name
}
function Child () {
  Parent.call(this, 'parent name')
}
Child.prototype = Parent.prototype // 把指向父类实例 改成 指向 父类原型
Child.prototype.constructor = Child

child1.name[0] = 'change'
console.log(child1.name[0]) // change
console.log(child2.name[0]) // parent name
child2.getName() // ['parent name']

这样的方式看起来不错,但是其实存在一个不容易发先的问题,由于子类原型和父类原型指向的是同一个对象,我们对子类原型的操作就会影响到父类原型,比如这里给 Child.prototype 增加一个 getName 方法,就会导致 Parent.prototype 同时也增加或者覆盖一个 getName 方法,所以我们这里可以给 Parent.prototype 做一个浅拷贝

function Parent (name) {
  this.name = [name]
}
Parent.prototype.getName = function () {
  return this.name
}
function Child () {
  // 构造函数继承
  Parent.call(this, 'parent name') 
}
//原型链继承
// Child.prototype = new Parent()
/**
 * 当然这个Object.create可以用一个空的构造函数来去取代它
 * function create(proto) {
 *  function F () {}
 *  F.prototype = proto
 *  return new F()
 * }
 * Child.prototype = create(Parent.prototype)
*/
Child.prototype = Object.create(Parent.prototype)  // 将`指向父类实例`改为`指向父类原型`
Child.prototype.constructor = Child

//测试
const child = new Child()
const parent = new Parent()
child.getName() // ['parent name']
parent.getName() // 报错, 找不到getName()

Class

  • 定义:构造函数语法糖
  • 原理:类本身就是构造函数,所有的方法都定义在 prototype
  • 方法和关键字
    • constructor():构造函数,new的时候(生成实例)时自动调用
    • extends:继承父类
    • super:新建父类的this
    • static:定义静态属性方法
    • get:拦截函数的取值行为
    • set:拦截函数的存值行为
  • 属性
  • proto:构造函数的继承(指向父类)
  • proto.proto:子类的原型的原型,就是父类的原型(指向父类的__proto__)
  • prototype.proto:属性方法的继承(指向父类的prototype)
  • 静态属性:定义后完成赋值属性,不能被实例继承,只可以通过类来调用
  • 静态方法:使用static定义方法,不能被实例继承,只能通过类来调用(方法中的this指向类,不是实例)
  • 继承
    • 实质
      • es5:先创造子类实例的this,再把父类的属性方法添加到this
      • es6:先把父类实例的属性方法加到this上面(调用super()),在用子类构造函数修改this
    • super
      • 作为函数调用:只能在构造函数里使用super(),内部this指向继承的当前子类(super()调用后可在构造函数中使用this
      • 作为对象调用:普通方法中指向父类的原型对象,在静态方法中指向父类
    • 显示定义:就是用 constructor () { super()} 定义继承父类
    • 子类继承父类:子类使用父类的属性方法时,必须在构造函数中调用 super(),要不然拿不到父类的 this
      • 父类静态属性方法可被子类继承
      • 子类继承父类后,可从那个 super 上调用父类静态属性方法
    • 实例: 类相当于实例的原型,所有在类中定义的属性方法都会被实例继承
      • 显式指定属性方法:使用 this指定到自身(可以用Class.hasOwnProperty检测)
      • 隐式指定属性方法:直接声明定义在对象原型上(用 Class..proto.hasOwnProperty检测)

extends关键字主要用于类声明或者类表达式中创建一个类,这个类就是子类。在这里 constructor就是表示构造函数,一个类只能有一个构造函数,如果没有显式指定构造方法,就会默认添加一个 constructor 方法

class Rectangle {
  // constructor
  constructor (height, width) {
    this.height = height
    this.width = width
  }
  // Getter
  get area () {
    return this.calcArea()
  }
  // Method
  calcArea () {
    return this.height * this.width
  }
}

const rectangle = new Rectangle(10, 20)
console.log(rectangle.area) // 200

----------------------------------

//继承

class Square extends Rectangle {
  constructor (length) {
    // 如果子类中存在构造函数,则需要在使用 this 之前首先调用 super()
    super(length, length)
    this.name = 'Square'
  }
  get area () {
    return this.height * this.width
  }
}

const square = new Square(10)
console.log(square.area) // 100

extends继承的核心代码其实也是和寄生组合继承是一样的

function _inherits (child, parent) {
  /**
   * 创建对象,创建父类原型的副本
   * 增强对象,重写原型丢失的默认的constructor属性
   * 指定对象,把新创建的对象赋值给子类的原型
  */
  child.prototype = Object.create(parent && parent.prototype, {
    constructor: {
      value: child,
      ennumerable: false,
      writable: true,
      configurable: true
    }
  })

  if (parent) {
    Object.setPrototypeOf 
      ? Object.setPrototypeOf(child, parent) 
      : child.__proto__ = parent
  }
}

回顾

  • 一般下继承最容易想到的就是 原型链继承,就是把子类的原型指向父类的实例来继承父类的属性和方法,但是原型链继承的缺陷在于 对子类实例继承的引用类型的修改会影响到所有的实例对象以及无法向父类的构造方法传参
  • 接着就是引入 构造函数继承,通过在子类构造函数中调用父类构造函数并传入子类的this来获取父类的属性和方法,但是构造喊你输继承也是存在缺陷的,构造函数继承 不能继承到父类原型链上的属性和方法
  • 综合两种继承的优点,实现了 组合式继承,但是组合式继承也是存在问题的,就是 每次创建子类实例都执行了两次父类的构造方法
  • 最后 把子类原型指向父类实例 改成 子类原型指向父类原型的浅拷贝 来解决这个问题,最终实现了 寄生组合继承

BlingSu avatar Oct 29 '20 10:10 BlingSu