JavaScript ES5继承ES6 class继承

Posted 月岛蘑菇

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript ES5继承ES6 class继承相关的知识,希望对你有一定的参考价值。

ES5继承、ES6 class继承

原型链继承

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function() {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType() // 原型链继承
SubType.prototype.getSubValue = function() {
  return this.subproperty
}
let instance = new SubType()
console.log(instance.getSuperValue()) // true

缺点:

  1. 实例无法向父类构造函数传参
  2. 继承单一
  3. 所有新实例都会共享父类实例的属性(原型上的属性共享,一个是你修改了原型属性,另一个实例的原型属性也会被修改

    借用构造函数继承

    function Person(name) {
      this.name = name
      this.sum = function() {
     alert(this.name)
      }
    }
    function Con() {
      // 利用call apply在函数内部将父类构造函数引入子类函数
      Person.call(this, \'name\')
      this.age = 12
    }
    var con1 = new Con()
    console.log(con1.name)    // name
    console.log(con1.age) // 12
    console.log(con1 instanceof Person) // false

    特点:

  4. 可以传参
  5. 可以继承多个构造函数属性

缺点:

  1. 只能继承父类构造函数的属性
  2. 无法实现构造函数的复用
  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)  // 第一次调用SuperType
      this.age = age
    }
    // 继承方法
    SubType.prototype = new SuperTuper(\'Nich\', 29) // 第二次调用SuperType
    SubType.prototype.sayAge = function() {
      console.log(this.age)
    }
    
    let instance1 = new SubType()
    instance1.colors.push(\'black\')
    console.log(instance1.colors) // "red, blue, green, black"
    instance1.sayName() // \'Nich\'
    instance1.sayAge() // 29
    
    let instance2 = new SubType(\'Greg\', 27)
    console.log(instance2.colors) // "red, blue, green"
    instance2.sayName() // \'Greg\'
    instance2.sayAge()  // 27

    组合继承弥补了原型链和盗用构造函数的不足,是JS中使用最多的集成模式
    组合继承也保留了instanceof操作符和isPrototypeOf()的识别合成对象的能力
    组合继承的效率问题:父类构造函数始终会被调用两次,一次是在创建子类原型时调用,另一次时在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就可以了


原型式继承与Object.create()

// 当Object.create()只传一个参数时,与以下效果相同
function object(o) {
  function F(){}
  F.prototype = o
  return new F()
}
let person = {
  name: \'Nich\',
  friends: [\'Van\', \'Court\']
}
let anotherPerson = object(person)
// 等同于let anotherPerson = Object.create(person)
console.log(anotherPerson.name) // Nich (通过原型链向上查找)
anotherPerson.name = \'Greg\'
anotherPerson.friends.push(\'Rob\')

let yetAnotherPerson = object(person)
yetAnotherPerson.name = \'Linda\'
yetAnotherPerson.friends.push(\'Barbie\')

console.log(person.friends) // Court, Van, Rob, Barbie

Object.create()的第二个参数与Object.defineProperties第二个参数一样: 每个新增属性都通过各自的描述符来描述,以这种方式添加的属性会遮蔽原型上的同名属性

let person = {
  name: \'Nich\',
  friends: [\'Van\', \'Court\']
}
let anotherPerson = Object.create(person, {
  name: {
    value: \'Greg\'
  }
})
console.log(anotherPerson.name) // \'Greg\'

原型式继承非常适合不需要单独创建构造函数,担任需要在对象间共享信息的场合,但是属性中包含的引用值始终会在相关对象间共享,和使用原型模式式一样的


寄生式继承

寄生式继承背后的思路类似于寄生构造函数和工厂模式:
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

此函数接收一个参数,就是新对象的基准对象
function createAnother(original) {
  let clone = object(original)
  clone.sayHi = function() {
    console.log(\'hi\')
  }
  return clone
}
let person = {
  name: \'Nick\',
  friends: [\'Bob\', \'Van\']
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // hi

通过寄生式继承给对象添加函数会导致函数难以重用, 与构造函数模式类似


寄生式组合继承

function SubType(name, age) {
  SuperType.call(this, name)
  this.age = age
}
function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}
inheritPrototype(SubType, SuperType)
SubType.prototype.sayAge = function() {
  console.log(this.age)
}

寄生式组合继承避免了多次调用SuperType构造函数,避免了SubType.prototype上不必要也用不到的属性,因此效率更高。而且原型键保持不变,instanceof和isPrototypeOf()依然有效
寄生式组合继承可以算是引用类型继承的最佳模式


ES6 class继承

基本用法:class之间通过使用extends关键字完成继承,这比通过修改原型链实现继承方便得多

class Vehicle {}
class Bus extends Vehicle{}
let b = new Bus()
console.log(b instanceof Bus) // true
console.log(b instanceof Vehicle) // true
// 继承普通的构造函数
function Person() {}
class Engineer extends Person {}
let e = new Engineer()
console.log(e instanceof Engineer)  // true
console.log(e instanceof Person)  // true

在类构造函数中使用super可以调用父类的构造函数

class Vehicle {
  constructor() {
    this.hasEngune = true
  }
}
class Bus extends Vehicle {
  constructor() {
    不要在super之前引用this,否则会抛出ReferenceError
    super() // 相当于super.constructor
    console.log(this instanceof Vehicle)  // true
    console.log(this) // Bus { hasEngine: true }
  }
}
// 静态方法
class Vehicle {
  static identify() {
    console.log(\'vehicle\')
  }
}
class Bus extends Vehicle{
  static identify() {
    super.identify()
  }
}
Bus.identify()  // vehicle

使用super的时候需要注意的问题

  • 不能super只能在派生类构造函数和静态方法中使用
  • 不能单独引用super关键字,要么用它调用构造函数,要么用它引用静态方法
  • super会调用父类构造函数,并将返回的实例赋值给this
  • super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入

    class Vehicle{
    constructor(licensePlate) {
      this.licensePlate = licensePlate
    }
    }
    class Bus extends Vehicle {
    constructor(lP) {
      super(lP)
    }
    }
    console.log(new Bus(\'XF18\')) // Bus {licensePlate: "XF018"}
  • 如果没有定义类构造函数,在实例化派生类的时候会调用super,并且会传入所有传给派生类的参数

    class Vehicle{
    constructor(lp) {
      this.lp = lp
    }
    }
    class Bus extends Vehicle {}
    
    console.log(new Bus(\'XF18\'))  // Bus {lp: "XF18"}
  • 在类构造函数中,不能在super()之前引用this
  • 如果在派生类中显式地定义了构造函数,则要么必须在其中调用super,要么必须在其中返回一个对象

    class Vehicle{}
    class Car extends Vehicle{}
    class Bus extends Vehicle{
    constructor() {
      super()
    }
    }
    class Van extends Vehicle{
    constructor() {
      return {}
    }
    }
    console.log(new Car())  // Car {}
    console.log(new Bus())  // Bus {}
    console.log(new Van())  // {}

    抽象基类

    有时候可能需要这样的一种类: 它可以供其它类继承,但是当时本事不能被实例化
    虽然专门支持这种类的语法,但是可以通过new.target关键字进行判断实现
    new.target保存通过new关键字调用的类,在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化

    class Vehicle {
    constructor() {
      console.log(new.target)
      if(new.target === Vehicle) {
        throw new Error(\'Vehicle cannot be directly instantiated\')
      }
    }
    }
    class Bus extends Vehicle {}
    
    new Bus // Bus{}
    new Vehicle  // Vehicle 
    // Error: Vehicle cannot be directly instantiated

    通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法,因为原型方法在调用类构造函数之前就已经存在了,所以可以通过this关键字来查询相应方法

    class Vehicle {
    constructor() {
      if(new.target === Vehicle) throw new Error(\'Vehicle cannot be directly instantiated\')
      if(!this.foo) throw new Error(\'Inheriting class must define foo()\')
      console.log(\'success\')
    }
    }
    class Bus extends Vehicle{
    foo() {}
    }
    class Van extends Vehicle{}
    new Bus() // Bus {} success
    new Van() // Error: Inheriting class must define foo()

    继承内置类型

    ES6类为继承内置应用类型提供了顺畅的机制,可以方便的扩展内置类型

    class SuperArray extends Array {
    shuffle() {
      for (let i = this.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1))
        ;[this[i], this[j]] = [this[j], this[i]]
      }
    }
    }
    let a = new SuperArray(1,2,3,4,5)
    console.log(a instanceof Array) // true
    console.log(a instanceof SuperArray)  // true
    console.log(a)  // SuperArray(5) [1, 2, 3, 4, 5]
    a.shuffle()
    console.log(a) // SuperArray(5) [3, 5, 4, 2, 1]

    有些内置类型的方法会返回新的实例,默认情况下返回的实例类型与原始实例的类型是一致的

    class SuperArray extends Array{}
    let a1 = new SuperArray(1, 2, 3, 4, 5)
    let a2 = a1.filter(x => !!(x%2))
    console.log(a1) // SuperArray(5) [1, 2, 3, 4, 5]
    console.log(a2) // SuperArray(3) [1, 3, 5]
    console.log(a1 instanceof SuperArray) // true
    console.log(a2 instanceof SuperArray) // true

    如果要覆盖这个默认行为,可以覆盖Symbol.species访问器

    class SuperArray extends Array{
    static get [Symbol.species] () {
      return Array
    }
    }
    let a1 = new SuperArray(1, 2, 3, 4, 5)
    let a2 = a1.filter(x => !!(x%2))
    console.log(a1) // SuperArray(5) [1, 2, 3, 4, 5]
    console.log(a2) // SuperArray(3) [1, 3, 5]
    console.log(a1 instanceof SuperArray) // true
    console.log(a2 instanceof SuperArray) // false

    类混入

    把不同类的行为集中到一个类,ES6虽然没有显式的对类混入支持,但是现有的特性可以模仿这种行为

    Object.assign()是为了混入对象行为而设计的
    只有在需要混入类的行为时才有必要自己实现混入表达式
    如果混入的只是对象的属性,那么使用Object.assign()即可

    如果Person类需要组合A B C,则需要某种机制实现B继承A,C继承B,然后Person继承C

    // 可嵌套的连续继承
    class Vehicle{}
    let FooMixin = (SuperClass) => class extends SuperClass{
    foo() {
      console.log(\'foo\')
    }
    }
    let BarMixin = (SuperClass) => class extends SuperClass{
    bar() {
      console.log(\'bar\')
    }
    }
    let BazMixin = (SuperClass) => class extends SuperClass{
    baz() {
      console.log(\'baz\')
    }
    }
    // 连续继承
    class Bus extends FooMixin(BarMixin(BazMixin(Vehicle))) {}
    let b = new Bus()
    b.foo() // foo
    b.bar() // bar
    b.baz() // baz

    以上连续继承可以通过一个辅助函数,把嵌套展开

    function mix(BaseClass, ...Mixins) {
    return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass)
    }
    
    class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
    let b = new Bus()
    b.foo() // foo
    b.bar() // bar
    b.baz() // baz

以上是关于JavaScript ES5继承ES6 class继承的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript之ES6中的class与继承

ES6新特性:使用新方法定义javascript的Class

ES5和ES6中的继承 图解

ES5和ES6中的继承

ES6:class的定义与继承,从ES5转换成ES6

es5继承和es6类和继承