面试(三)之继承

113 阅读5分钟

摘录于js继承的六种实现方式

先定义一个父类

function SuperType () {
    // 属性
    this.name = 'SuperType'
}
// 原型方法
SuperType.prototype.sayName = function() {
    return this.name
}

原型链继承

将父类的实例作为子类的原型

// 子类
function SubType () {
        this.subName = 'SubType' // 子类属性
}

SubType.prototype = new SuperType() 
// 这里实例化一个SuperType时,实际上执行了两步
// 1.新创建的对象复制了父类构造函数内的所有属性及方法
// 2.并将原型__proto__指向了父类的原型对象

SubType.prototype.saySayName = function () {
    return this.subName
}

// 子类实例
let instance = new SubType()

instance instanceof SubType;     // true
instance instanceof SuperType;     // true

优点

  • 继承了父类的模板,又继承了父类的原型对象
    缺点
  • 无法实现多继承
  • 如果要新增原型属性和方法,则必须放在SubType.prototype = new SuperType('SubType');语句之后执行
  • 来自原型对象的所有属性被所有实例共享
// 父类
function SuperType () {
    this.colors = ['red', 'blue', 'green']
    this.name = 'SuperType'
}
// 子类
function SubType () {}

// 原型链继承
SubType.prototype = new SuperType()

// 实例1
var instance1 = new SubType()
instance1.colors.push('black')
instance1.name = 'change-super-type-name'
console.log(instance1.colors)      // ['red', 'blue', 'green', 'black']
console.log(instance1.name)      // change-super-type-name
// 实例2
var instance2 = new SubType()
console.log(instance2.colors)      // ['red', 'blue', 'green', 'black']
console.log(instance2.name)      // SuperType
  • 创建子类实例时,无法向父类构造函数传参,或者说是,没办法在不影响所有对象实例的情况下,向超类的构造函数传参。

构造继承

在子类型的构造函数内部调用父类型构造函数

// 父类
function SuperType (name) {
    this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () {  // 父类原型方法
    return this.name
}

// 子类
function SubType () {
    // 调用 SuperType 构造函数
    SuperType.call(this, 'SuperType')  // 在子类构造函数中,向父类构造函数传参
    // 为了保证子父类的构造函数不会重写子类的属性,需要在调用父类构造函数后,定义子类的属性
    this.subName = 'SubType' // 子类属性
}
// 子类实例
let instance = new SubType() // 运行子类构造函数,并在子类构造函数中运行父类构造函数,this绑定到子类

优点:
实现多继承,创建子类实例时,可以向父类传递参数
缺点:

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

组合继承

就是将原型链继承与构造函数继承组合在一起,从而发挥两者之长的一种继承模式。

// 父类
function SuperType (name) {
    this.colors = ['red', 'blue', 'green']
    this.name = name
}
SuperType.prototype.sayName = function () {
    return this.name
}

// 子类
function SubType (name, subName) {
    // 调用 SuperType 构造函数
    SuperType.call(this, name) // 第二次调用 SuperType
    this.subName = subName
}

// 第一次调用 SuperType
SubType.prototype = new SuperType()  // 重写原型对象,代之以一个新类型的实例

SubType.prototype.constructor = SubType  // 组合继承需要修复构造函数指向
SubType.prototype.saySubName = function () {
    return this.subName
}

// 子类实例
let instance = new SubType('An', 'sisterAn')
instance.colors.push('black')
console.log(instance.colors)  // ['red', 'blue', 'green', 'black']
instance.sayName() // An
instance.saySubName() // sisterAn

let instance1 = new SubType('An1', 'sisterAn1')
console.log(instance.colors)  // ['red', 'blue', 'green']
instance1.sayName()  // An1
instance1.saySayName()  // sisterAn1
// instanceof:instance 的原型链是针对 SuperType.prototype 进行检查的
instance instanceof SuperType // true
instance instanceof SubType // true

// isPrototypeOf:instance 的原型链是针对 SuperType 本身进行检查的
SuperType.prototype.isPrototypeOf(instance)  // true
SubType.prototype.isPrototypeOf(instance)  // true

优点
弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法,不存在引用属性共享问题,可传参,可复用 缺点
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

寄生组合继承

通过寄生方式,砍掉父类的实例属性。这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免组合继承的缺点。

借用构造函数继承属性,通过原型链的混成形式来继承方法

// 父类
function SuperType (name) {
    this.colors = ['red', 'blue', 'green']
    this.name = name 
}
SuperType.prototype.sayName = function () {
    return this.name
}

// 子类
function SubType (name, subName) {
    // 调用 SuperType 构造函数
    SuperType.call(this, name) // 第二次调用 SuperType,继承实例属性
    this.subName = subName
}
// 第一次调用SuperType继承原型属性
SubType.prototype = Object.create(SuperType.prototype)

SubType.prototype.constructor = SubType

let instance = new SubType('An', 'sisterAn')

优点

  • 只调用一次SuperType构造函数,只创建一份父类属性
  • 原型链保持不变
  • 能够正常使用instanceof与isPrototypeOf

原型式继承

将子类的原型设置为父类的原型

// 父类
function SuperType (name) {
    this.colors = ['red', 'blue', 'green']
    this.name = name 
}
SuperType.prototype.sayName = function () {
    return this.name
}

// 第一步
// 子类,通过call继承父类的实例属性和方法,不能继承原型属性/方法
function SubType (name, subName) {
    SuperType.call(this, name) // 调用SuperType的构造函数,并向其传参
    this.subName = subName
}

// 第二步
// 解决 call 无法继承父类原型属性/方法的问题 
// Object.create 方法接受传入一个作为新创建对象的原型的对象,创建一个拥有指定原型和若干个指定属性的对象 
// 通过这种方法指定的任何属性都会覆盖原型对象上的同名属性
SubType.prototype = Object.create(SuperType.prototype, {
    constructor: {  // 注意指定 SubType.prototype.constructor = SubType
        value: SubType,
        enumerable: false,
        writable: true,
        configurable: true
    },
    run: {
        value: function() {  // override
            SuperType.prototype.run.apply(this, arguments)
            // call super
            // ...
        },
        enumerable: true,
        configurable: true,
        writable: true
    }
})

// 第三步
// 最后:解决SubType.prototype.constructor === SuperType的问题
// 这里,在上一步已经指定,不需要再操作
// SubType.prototype.constructor = SubType

var instance = new SubType('An', 'sistenAn')

多继承

如果希望能多继承,可使用混入的方式

function SuperType() {}
function OtherSuperType() {}

// 多继承子类
function AnotherType () {
    SuperType.call(this)  // 继承SuperType的实例属性和方法
    OtherSuperType.call(this)  // 继承OtherSuperType的实例属性和方法
}

// 继承一个类
AnotherType.prototype = Object.create(SuperType.prototype)

// 使用Object.assign混合其它
Object.assign(AnotherType.prototype, OtherSuperType.prototype)
// Object.assign 会把 OtherSuperType 原型上的函数拷贝到 AnotherType 原型上,使 AnotherType 的所有实例都可用 OtherSuperType 的方法

// 重新指定 constructor
AnotherType.prototype.constructor = AnotherType

AnotherType.prototype.myMethod = function() {
    // do a thing
}

let instance = new AnotherType()

最重要的部分是:

  • SuperType.call 继承实例属性和方法
  • 用Object.create()来继承原型属性与方法
  • 修改SubType.prototype.constructor的指向