一文搞懂JavaScript继承!🔥

87 阅读4分钟

一文搞懂JavaScript继承!🔥

大家好我是雪人⛄

最近面试的时候被问到了JS的继承,发现自己对继承的了解非常肤浅。面试官引导半天才写出来一个比较拉跨的方法🤡。

太菜了,搜集总结了一些继承的常见方案,在这里分享给大家😀。

原型链

要搞懂继承大家需要先搞懂JS的原型与原型链,构造函数,原型和实例之间的关系。

简单来说就是 实例.__proto__ == 原型原型.constructor == 构造函数构造函数.prototype == 原型

不太了解的建议先去学习一下再看这篇文章,这里就不过多赘述了。

常见方案

大家主要学会 ES6 的继承语法,ES5 中的寄生组合式继承和增强寄生组合即可。

原型链继承

function Parent() {
    this.name = "parent"
}
Parent.prototype.sex = "man"function Child() {
    this.age = 18
}
​
// 原型链继承方法
// 这样就可以使 Child 的实例继承 Parent 上的属性以及 Parent.prototype 的属性
Child.prototype = new Parent()
​
let child = new Child()
​
console.log(child.age) // 18
console.log(child.name) // parent
console.log(child.sex) // man

缺点

  • 不同实例中的引用数据类型会被篡改
function Parent() {
    this.obj = {
        name: "parent"
    }
}
​
function Child() {}
​
Child.prototype = new Parent()
​
let child1 = new Child()
child1.obj.name = "child"
let child2 = new Child()
​
console.log(child1.obj.name) // child
// name 收到了 child1 中修改的影响
console.log(child2.obj.name) // child

借用构造函数继承

function Parent() {
    this.obj = {
        name: "parent"
    }
}
​
function Child() {
    // 调用父类 constructor 将属性全部复制
    Parent.call(this)
}
​
let child1 = new Child()
child1.obj.name = "child"
let child2 = new Child()
​
console.log(child1.obj.name) // child
console.log(child2.obj.name) // parent

缺点

  • 每个子类都有父类实例的副本,可能会影响性能
  • 只能继承父类的实例属性方法,无法继承父类原型链的内容
function Parent() {
    this.obj = {
        name: "parent"
    }
}
​
Parent.prototype.sex = "man"function Child() {
    Parent.call(this)
}
​
let child  = new Child()
// 无法继承父类原型链上的属性
console.log(child.sex) // undefined

组合继承

  • 组合继承就是结合了构造函数继承和原型链继承
function Parent() {
    this.obj = {
        name: "parent"
    }
}
​
Parent.prototype.sex = "man"function Child() {
    Parent.call(this)
}
​
Child.prototype = new Parent()
​
let child1 = new Child()
child1.obj.name = "child"
let child2 = new Child()
​
console.log(child1.obj.name) // child
console.log(child2.obj.name) // parent
console.log(child1.sex) // man

缺点

  • 子类的原型链上会出现重复的属性
child1
  ├─ obj: {name: "child"} (实例属性)
  └─ __proto__ (Child.prototype)
       ├─ obj: {name: "parent"} (Parent 构造函数中的实例属性)
       └─ __proto__ (Parent.prototype)
            └─ sex: "man" (Parent 构造函数的原型属性)
                 └─ __proto__ (Object.prototype)
  • 从上图我们可以看出父类的原型链被我们放在了子类的原型的原型上。而 child 实例上也被我们复制了一份属性,导致属性重复。访问的时候优先访问 this 上的方法,所以 child1.__proto__ 这一步就不需要。

原型式继承

  • 利用一个空对象作为中介,消除重复的属性。
// 这里展示一下思路
// ES5 中可以使用 Object.create() 代替
function createObject(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
function Parent() {
    this.obj = {
        name: "parent"
    }
}
​
Parent.prototype.sex = "man"function Child() {
    Parent.call(this)
}
​
// 可以使用 Object.create(new Parent())
Child.prototype = createObject(new Parent())
​
let child1 = new Child()
child1.obj.name = "child"
let child2 = new Child()
​
console.log(child1.obj.name) // child
console.log(child2.obj.name) // parent
console.log(child1.sex) // man
child1
  ├─ obj: {name: "child"} (实例属性)
  └─ __proto__ (返回的F函数实例)
       ├─ __proto__ (Parent.prototype)
       │    ├─ obj: {name: "parent"} (Parent 构造函数中的实例属性)
       │    └─ __proto__ (Object.prototype)
       └─ constructor: F
  • 从上图我们可以看出重复属性已被清除

寄生式继承

  • 可以给我们要继承的对象赋予新的原型属性
function createObject(obj){
  function F(){}
  F.prototype = obj
  const clone = new F()
  // 可以在此处增添子类原型链上的属性
  clone.name = "cloneName"
  return clone
}
  • 就是能多了个能在第一层原型上加属性,变了一个名字。

寄生组合式继承

  • 封装一个继承的方法
function inheritPrototype(child, parent){
    // 新建一个以 parent.prototype 为原型的空对象
    let prototype = Object.create(parent.prototype)
    // 弥补因重新 prototype 失去的 constructor
    prototype.constructor = child
    // 将 child 的 prototype 指向新建的空对象
    child.prototype = prototype
}
function Parent() {
    this.name = "parent"
    this.hobby = ["js"]
}
​
function Child() {
    Parent.call(this)
}
​
inheritPrototype(Child, Parent)
​
let child1 = new Child()
child1.hobby.push("ts")
let child2 = new Child()
​
console.log(child1.hobby) // [ 'js', 'ts' ]
console.log(child2.hobby) // [ 'js' ]
  • 如果觉得中间总是多一层 prototype 不方便也可以换一种写法
function inheritPrototype(child, parent) {
    child.prototype = parent.prototype
    child.constructor = child
}

缺点

  • 子类上原有 prototype 的属性会被覆盖

增强寄生组合

  • 如果我们想保留子类 prototype 的属性,可以更改一下 inheritPrototype 函数。
function inheritPrototype(child, parent) {
    let proto = parent.prototype;
    // 把子类原型的所有属性复制到一起
    Object.keys(child.prototype).forEach(key => {
        Object.defineProperty(proto, key, { value: child.prototype[key] })
    })
    child.prototype = proto;
    child.constructor = child;
}
function Parent() {
    this.name = "parent"
    this.hobby = ["js"]
}
​
Parent.prototype.parentName = "parentName"function Child() {
    Parent.call(this)
}
​
Child.prototype.childName = "childName"inheritPrototype(Child, Parent)
​
let child1 = new Child()
child1.hobby.push("ts")
let child2 = new Child()
​
console.log(child1.hobby) // [ 'js', 'ts' ]
console.log(child2.hobby) // [ 'js' ]
console.log(child1.childName) // childName
console.log(child2.parentName) // parentName

ES6继承

  • 使用 ES6 的 Class 语法糖可以轻松实现继承
class Parent {
    constructor() {
        this.name = "parent"
        this.age = 16
    }
    get info() {
        return this.name + " " + this.age
    }
}
​
Parent.prototype.hobby = ['js']
​
class Child extends Parent{
    constructor() {
        super()
        this.sex = "man"
    }
}
​
const child = new Child()
console.log(child.info) // parent 16
console.log(child.sex) // man
console.log(child.hobby) // [ 'js' ]

总结

大家只要学会这些常见的方案应付面试就没啥大问题了。

ES5 大家学会 寄生组合式继承增强寄生组合 根据不同场景去使用即可。

有不准确的地方欢迎大家指正😊

如果对你有帮助的话,请帮我点个赞吧😀