一文搞懂js继承

94 阅读4分钟

一文搞懂js继承

原型链继承

function Father() {
    this.name = 'jack'
    this.property = ['money', 'house']
}
Father.prototype.getName = function() {
    return this.name
}
function Son() {
    this.age = 12
}
Son.prototype = new Father() // 修改Son的原型指向Father的实例
const son1 = new Son()
const son2 = new Son()
son1.property.push('game')
console.log(Son.prototype) // Father { name: 'jack', property: [ 'money', 'house', 'game' ] }
console.log(son1.name) // jack
console.log(son2.name) // jack
console.log(son1.getName()) // jack
console.log(son1.property) // [ 'money', 'house', 'game' ]
console.log(son2.property) // [ 'money', 'house', 'game' ]

原型链继承就是通过改变子类property属性指向,通过上面的例子可以发现,这种方式存在很明显的弊端:实例共享了原型对象引用类型的属性,一个实例修改了这个属性,其他实例的属性也会跟着变

构造函数继承

function Father() {
    this.name = 'jack'
    this.property = ['money', 'house']
}
Father.prototype.getName = function() {
    return this.name
}
function Son() {
    Father.call(this)
}
const son1 = new Son()
const son2 = new Son()
son1.property.push('game')
console.log(son1) // Son { name: 'jack', property: [ 'money', 'house', 'game' ] }
console.log(son2) // Son { name: 'jack', property: [ 'money', 'house' ] }
console.log(son1.getName()) // TypeError: son1.getName is not a function

构造函数继承就是通过在子类构造函数中调用父类构造函数,并通过call改变父类中的this指向。 通过上面的例子,解决了原型链继承子类实例共享父类引用类型属性的问题,但是又出现了一个问题,就是子类实例只能访问父类构造函数中的属性,无法访问父类原型上定义的方法。

组合继承

function Father() {
    this.name = 'jack'
    this.property = ['money', 'house']
}
Father.prototype.getName = function() {
    return this.name
}
function Son() {
    Father.call(this)
}
Son.prototype = new Father()
const son1 = new Son()
const son2 = new Son()
son1.name = 'mark'
son1.property.push('game')
console.log(son1) // Father { name: 'mark', property: [ 'money', 'house', 'game' ] }
console.log(son2) // Father { name: 'jack', property: [ 'money', 'house' ] }
console.log(son1.getName()) // mark

组合继承顾名思义就是前面两种继承方式的组合,通过以上例子,组合继承解决了原型链继承和构造函数继承的问题,但是也出现了新的问题,就是调用了两次父类构造函数

原型式继承

const Father = {
    name: 'jack',
    property: ['house', 'money'],
    getName: function () {
        return this.name
    }
}
const son1 = Object.create(Father)
const son2 = Object.create(Father)
son1.name = 'mark'
son1.property.push('game')
console.log(son1) // { name: 'mark' }
console.log(son2) // {}
console.log(son1.name) // mark
console.log(son2.name) // jack
console.log(son1.property) // [ 'house', 'money', 'game' ]
console.log(son2.property) // [ 'house', 'money', 'game' ]
console.log(son1.getName()) // mark
console.log(son2.getName()) // jack

原型式继承是通过js原生提供Object.create()来实现的,这种方式的弊端也很明显,子类实例会共享父类引用类型的属性。

下面来剖析Object.create()到底干了啥,自定义一个Object函数

function Object(obj) {
    // 只能是object或者null
    if (typeof obj !== 'object' && obj !== null && typeof obj !== 'function') {
        throw new Error('Uncaught TypeError: Object prototype may only be an Object or null')
    }
    function F() {}
    F.prototype = obj
    const f = new F()
    return f
}

这种方式的本质还是类似于原型链继承,但是相对于原型链继承,这种方式没有调用父类的构造函数。下面来看一种在原型式继承基础上进行优化的一种方式

寄生式继承

const Father = {
    name: 'jack',
    property: ['house', 'money'],
    getName: function () {
        return this.name
    }
}
function clone(original) {
    const cloneObj = Object.create(original)
    cloneObj.getProperty = function() {
        return this.property
    }
    return cloneObj
}
const son1 = clone(Father)
const son2 = clone(Father)
son1.name = 'mark'
son1.property.push('game')
console.log(son1.name) // mark
console.log(son2.name) // jack
console.log(son1.property) // [ 'house', 'money', 'game' ]
console.log(son2.property) // [ 'house', 'money', 'game' ]
console.log(son1.getProperty()) // [ 'house', 'money', 'game' ]

寄生式继承方式是在原型式继承基础上扩展了方法,但是依然没有解决子类实例共享原型对象中引用类属性的问题。

通过以上集中继承方式的分析,发现无非就是两个问题:

  • 子类实例共享原型对象中引用类型属性
  • 父类构造函数被调用了两次 并且两个问题都有对应的解决方案,需要做的就是将它们进行组合

寄生组合式继承

function Father() {
    this.name = 'jack'
    this.property = ['house', 'money']
}
Father.prototype.getName = function() {
    return this.name
}
function clone(parent, child) {
    child.prototype = Object.create(parent.prototype)
    child.prototype.constructor = child
}
function Son() {
    Father.call(this)
    this.friends = ['tony', 'ben']
}
clone(Father, Son)
Son.prototype.getFriends = function() {
    return this.friends
}
const son1 = new Son()
const son2 = new Son()
son1.name = 'mark'
son1.property.push('game')
son1.friends.push('lily')
console.log(son1.name) // mark
console.log(son2.name) // jack
console.log(son1.getName()) // mark
console.log(son2.getName()) // jack
console.log(son1.property) // [ 'house', 'money', 'game' ]
console.log(son2.property) // [ 'house', 'money' ]
console.log(son1.getFriends()) // [ 'tony', 'ben', 'lily' ]
console.log(son2.getFriends()) // [ 'tony', 'ben' ]

通过以上例子,将集中继承方式进行组合,解决前面的两个问题,因此,寄生组合式继承是比较完善的继承方式

ES6提供的extends关键字

在ES6中,js原生提供了extends关键字来实现继承,先看下它的使用方式

class Father {
    constructor(name) {
        this.name = name
    }
    getName = function() {
        return this.name
    }
}
class Son extends Father {
    constructor(name, age) {
        super(name)
        this.age = 12
    }
}
const son1 = new Son('mark') 
const son2 = new Son('jack')
console.log(son1.getName()) // mark
console.log(son2.getName()) // jack
console.log(son1) // Son { getName: [Function: getName], name: 'mark', age: 12 }
console.log(son2) // Son { getName: [Function: getName], name: 'jack', age: 12 }

其本质也是上面的寄生组合式继承,下面看下它的源码实现方式

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass)
  }
  // 子类的原型的__proto__指向父类的原型
  subClass.prototype = Object.create(superClass && superClass.prototype,  
    // 给子类添加 constructor属性 subclass.prototype.constructor === subclass
    {
      constructor:
      {
        value: subClass,
        enumerable: false,
        writable: true,
        configurable: true
      }
    }
  )
  if (superClass)
    //子类__proto__ 指向父类
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass
}