一文看懂 JS 继承

时间:2022-06-19
本文章向大家介绍一文看懂 JS 继承,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一文看懂 JS 继承

最近回顾 js 继承的时候,发现还是对一些概念不是很清晰。这里再梳理一下 JS 中继承的几种主要的方式,构造函数继承、原型链继承、组合继承以及原型式继承、寄生式继承、寄生组合式继承和 ES6 的 Class:

构造函数继承

构造函数继承没有用到 prototype 这种方式比较常见,定义和使用也较为简单,下面是一个例子?:

  • ? 可以定义私有属性方法
  • ? 子类可以传递参数给父类
  • ❌ 不能定义共享属性方法/或写在外面失去了封装性
function Parent(name, friends) {
    this.name = name
    this.friends = friends // ? 可以定义私有 引用类型不会被共享
    this.share = share // ❌ 可以定义公有 但需要放在外部
    this.log = log // ❌ 避免重复声明,为了复用需要放在外面
}
// ❌ 公有属性和方法定义在外面失去了封装性
let share = [1, 2, 3]
function log() {
    return this.name
}

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ? 可以在子类传递参数给父类
    this.gender = gender
}

原型链继承

原型链模式需要手动重新绑定 constructor 而且不能定义私有变量

  • ? 可以定义公有属性方法
  • ❌ 无论是定义还是继承都需要手动修改 constructor
  • ❌ 封装性一般
  • ❌ 不能定义私有属性方法
  • ❌ 没办法向父类传递参数
function Parent() {}
Parent.prototype = {
    constructor: Parent, // ❌ 需要手动绑定 constructor
    name: 'oli', // ❌ 不能定义私有属性,全部都是公有
    friends: ['alice', 'troy'], // ? 可以定义公有属性 所有实例都引用这个
    log: function() { // ? 方法被共享了
        return this.name
    }
}
// 也可以写成多个 Parent.prototype.func1 = function(){} 封装性更差 但不用修改 constructor
// ❌ 封装性一般

function Child() {} // ❌ 没办法向父类传递参数
Child.prototype = new Parent() // 使用 new 操作符创建并重写 prototype
Child.prototype.constructor = Child // ❌ 每次继承都需要手动修改 constructor 谁叫你是覆盖 prototype 属性呢

组合继承

上面两者结合即成为组合继承模式,这个是结合了两者的优势,在 ES6 的 class 出现之前的常用方法,??看看例子:

  • ? 公有的写在原型
  • ? 私有的写在构造函数
  • ? 可以向父类传递参数
  • ❌ 需要手动绑定 constructor
  • ❌ 封装性一般
  • ⚡ 重复调用父类性能损耗
function Parent(name, friends) {
    // ? 私有的写这里
    this.name = name // ? 可以定义私有属性
    this.friends = friends // ? 可以定义公有引用属性不会被共享
}
Parent.prototype = {
    // ? 公有的写这里
    constructor: Parent, // ❌ 需要手动绑定 constructor
    share: [1, 2, 3], // ? 这里定义的公有属性会被共享
    log: function() { // ? 方法被共享了
        return this.name
    }
}
// ❌ 封装性一般

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ? 可以向父类传递参数 ⚡ 这里又调用了一次 Parent
    this.gender = gender
}
Child.prototype = new Parent() // 使用 new 操作符创建并重写 prototype ⚡ 这里调用了一次 Parent
// 有方法避免多次调用直接去掉 new 操作符 转而写成 Child.prototype = Parent.prototype 这样并不好,虽然避免出现重复调用但导致修改子类 constructor 的时候父类也被修改了
Child.prototype.constructor = Child // ❌ 每次继承都需要手动修改 constructor 谁叫你是覆盖 prototype 属性呢
// 如果使用 Child.prototype = Parent.prototype 那么 constructor 子类父类是同一个

原型式继承

原型式继承直接使用 ES5 Object.create 方法,该方法的原理是创建一个构造函数,构造函数的原型指向对象,然后调用 new 操作符创建实例,并返回这个实例,本质是一个浅拷贝

  • ? 父类方法可以复用
  • ❌ 父类引用属性全部被共享
  • ❌ 子类不可传递参数给父类
let parent = {
    name: 'parent',
    share: [1, 2, 3], // ❌ 父类的引用属性全部被子类所共享
    log: function() { // ? 父类方法可以复用
        return this.name
    }
}

let child = Object.create(parent) // ❌ 子类不能向父类传递参数

寄生式继承

原型式继承的基础上为子类增加属性和方法

  • ? 父类方法可以复用
  • ? 增加了别的属性和方法
  • ❌ 父类引用属性全部被共享
  • ❌ 子类不可传递参数给父类
let parent = {
    name: 'parent',
    share: [1, 2, 3],
    log: function() {
        return this.name
    }
}

function create(obj) {
    let clone = Object.create(obj) // 本质上还是 Object.create
    clone.print = function() { // 增加一些属性或方法
        console.log(this.name)
    }
    return clone
}

let child = create(parent)

寄生组合式继承

杂糅了原型链式、构造函数式、组合式、原型式、寄生式而形成的一种方式:

组合继承的方法会调用两次 Parent,一次是在 Child.prototype = new Parent() ,一次是在 Parent.call()。这个是组合继承的唯一缺点,寄生组合式解决了这个问题:

  • ? 公有的写在原型
  • ? 私有的写在构造函数
  • ? 可以向父类传递参数
  • ? 不会重复调用父类
  • ❌ 需要手动绑定 constructor (如果重写 prototype)
  • ❌ 需要调用额外的方法封装性一般
function Parent(name, friends) {
    this.name = name
    this.friends = friends
}
Parent.prototype = {
    constructor: Parent, // ❌ 需要手动绑定 constructor
    share: [1, 2, 3],
    log: function() {
        return this.name
    }
}

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ⚡ 这里只需要调用一次 Parent
    this.gender = gender
}
// 上半部分和组合继承一样

let F = function() {} // 创建一个中介函数
F.prototype = Parent.prototype // 这个中介的原型指向 Parent 的原型
Child.prototype = new F() // 注意这里没有使用 new 操作符调用 Parent
Child.prototype.constructor = Child

对上述方法进行一个封装:

function Parent(name, friends) {
    this.name = name // ? 可以定义私有属性
    this.friends = friends // ? 可以定义公有引用属性不会被共享
}
Parent.prototype = {
    constructor: Parent, // ❌ 需要手动绑定 constructor
    share: [1, 2, 3], // ? 这里定义的公有属性会被共享
    log: function() { // ? 方法被共享了
        return this.name
    }
}

function Child(name, friends, gender) {
    Parent.call(this, name, friends) // ? 可以向父类传递参数 ⚡ 这里又调用了一次 Parent
    this.gender = gender
}

function proto(child, parent) {
    let clonePrototype = Object.create(parent.prototype)
    child.prototype = clonePrototype
    child.prototype.constructor = child
}

proto(Child, Parent)

ES6 class

class 的语法,就比较清晰了,能用 class 就用 class 吧:

class Parent {
    constructor(name, friends) { // 该属性在构造函数上,不共享
        this.name = name
        this.friends = friends
    }
    log() { // 该方法在原型上,共享
        return this
    }
}
Parent.prototype.share = [1, 2, 3] // 原型上的属性,共享

class Child extends Parent {
    constructor(name, friends, gender) {
        super(name, friends)
        this.gender = gender
    }
}

另外可以使用 get set 方法将 share 属性写入到原型中去

另外,class 是一种语法糖使用 babel 将其转化一下看看:

小结

最后上个图作为总结:

参考:

  • https://github.com/mqyqingfeng/Blog/issues/16
  • https://segmentfault.com/a/1190000015727237
  • https://segmentfault.com/a/1190000015216289