【重学前端】JavaScript中的继承

JavaScript中继承主要分为六种:类式继承(原型链继承)、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承

类式继承(原型链继承)

原理

原理:父类的实例赋值给子类的原型对象

function SuperClass () {
    this.superName = 'superName'
}
SuperClass.prototype.getSuperName = function () {
    console.log('superName is ' + this.superName)
}
function SubClass () {
    this.subName = 'subName'
}
// 继承父类
SubClass.prototype = new SuperClass()
SuperClass.prototype.getSubName = function () {
    console.log('subName is ' + this.subName)
}

const instance = new SubClass()
console.log(instance.superName) // superName
console.log(instance.subName) // subName
// 调用父类的方法
instance.getSuperName() // superName is superName
// 调用子类的方法
instance.getSubName() // subName is subName
console.log(instance instanceof SuperClass) // true
console.log(instance instanceof SubClass) // true

将父类实例赋值给子类的原型对象,子类的实例调用父类的方法的时候可以通过原型链的查找到父类的方法进行调用。

问题

看以下一个例子:

// 父类
function SuperClass () {
    this.list = [1, 2, 3]
    this.superName = 'super'
}
// 子类
function SubClass () {
    this.subName = 'sub'
}
// 子类的原型继承父类的实例
SubClass.prototype = new SuperClass()
// 实例化子类
const instance1 = new SubClass()
const instance2 = new SubClass()
// 修改实例1的list
instance1.list.push(4)
console.log(instance1.list) // [1, 2, 3, 4]
console.log(instance2.list) // [1, 2, 3, 4]

可以看到,当修改子类的实例1的list属性的时候,子类的实例2的list属性也被修改了

所以可以看出,由于子类是通过其原型prototype对父类进行实例化,继承父类,所以父类的属性如果是引用类型,则会在子类中被所有实例共用,因此一个子类中进行共有属性的修改,会影响其他子类

另外,类式继承无法向构造函数传参也是一大缺点

类式继承(原型链继承)的缺点

  • 父类的属性被所有子类共用(引用类型的属性一个子类的实例进行修改,其他子类会受到影响)
  • 无法向父类的构造函数传参

构造函数继承

// 构造函数继承
// 父类
function SuperClass (superName) {
    // 值类型的共有属性
    this.superName = superName
    // 引用类型共有属性
    this.list = [1, 2, 3]
}
// 父类的原型方法
SuperClass.prototype.getList = function () {
    return this.list
}
// 子类
function SubClass (name) {
    SuperClass.call(this, name)
}
// 实例化
const instance1 = new SubClass('instance1')
const instance2 = new SubClass('instance2')
// 修改实例1的list属性
instance1.list.push(4)
console.log(instance1.superName)
console.log(instance2.superName)
console.log(instance1.list) // [1, 2, 3, 4]
console.log(instance2.list) // [1, 2, 3]

instance1.getList() // Uncaught TypeError: instance1.getList is not a function

构造函数继承在子类中利用call方法(也可以用apply)更改函数的作用环境,将子类中的变量在父类的构造函数中执行了一遍,由于父类中是给this绑定属性的,因此子类就继承了父类的共有属性。

构造函数继承的优缺点

  • 优点
    • 解决了类式继承的引用类型的公有属性在一个子类的实例中修改会影响其他实例的问题
    • 可以向父构造函数进行传参
  • 缺点
    • 父类的原型方法不会被子类继承
    • 如果要被子类继承,就必须放在构造函数中,这样创建出来的每个实例都会单独拥有一份,违背了代码复用的原则

组合继承----类式继承+构造函数继承

组合继承就是结合了类式继承(原型链继承)和构造函数继承的优点

// 组合式继承
// 父类
function SuperClass (superName) {
    this.superName = superName
    this.list = [1, 2, 3]
}
// 父类的原型方法
SuperClass.prototype.getList = function () {
    return this.list
}
// 子类
function SubClass (superName, subName) {
    // 子类中继承父类构造函数中的属性
    SuperClass.call(this, superName)
    this.subName = subName
}
// 子类继承父类的原型方法
SubClass.prototype = new SuperClass()
SubClass.prototype.getSubName = function () {
    console.log(this.subName)
}
// 实例化
const instance1 = new SubClass('super_1', 'sub_1')
const instance2 = new SubClass('super_2', 'sub_2')
// 修改引用属性
instance1.list.push(4)
console.log(instance1.list) // [1, 2, 3, 4]
console.log(instance2.list) // [1, 2, 3]
// 调用父类的原型方法
console.log(instance1.getList()) // [1, 2, 3, 4]
console.log(instance2.getList()) // [1, 2, 3]

组合继承的优缺点

  • 优点
    • 子类可以向父类构造函数传参
    • 子类的实例中继承的父类引用类型的属性被修改不会影响其他实例
    • 子类可以继承父类的原型方法
  • 缺点
    • 继承的时候调用了两边父类的构造函数

原型式继承

2006年道格拉斯·克罗克福德大佬在《JavaScript中的原型式继承》中提出了一个观点:借助原型prototype可以根据已有的对象创建一个新对象,不必创建新的自定义对象类型

理解:

不论是类式继承还是构造函数继承,我们都在全局定义了一个子类,然后再进行继承操作,原型式继承可以直接在函数内部声明过度对象,继承原型后返回对象的实例(不需要在全局定义自定义对象)

// 原型式继承
function inheritObj (obj) {
    // 声明过渡的函数对象
    function F () {}
    // 过渡对象继承父类
    F.prototype = obj
    // 返回过渡对象的实例
    return new F()
}
// 父类
const parent = {
    name: 'parent',
    list: [1, 2, 3] 
}
const instance1 = inheritObj(parent)
const instance2 = inheritObj(parent)
instance1.name = 'instance1'
instance2.name = 'instance2'
console.log(instance1.name) // 'instance1'
console.log(instance2.name) // 'instance2'
// 修改引用类型的公有属性
instance1.list.push(4)
console.log(instance1.list) // [1, 2, 3, 4]
console.log(instance2.list) // [1, 2, 3, 4]

由于原型式继承内部实现原理与类式继承一致,将父类赋给子类的原型prototype,所以与类式继承一样,引用类型的公有属性在子类的一个实例被修改,会影响其他实例

寄生式继承

寄生式继承是原型式继承的二次封装,在封装过程中对继承的对象进行了某些扩展

// 寄生式继承
function createObj (obj) {
    let o = inheritObj(obj)
    o.prototype.getList = function () {
        return this.list
    }
    return o
}

寄生组合式继承----寄生式继承+构造函数继承

// 寄生组合式继承
function inheritPrototype (subClass, superClass) {
    // 复制一份父类的原型副本
    let proto = inheritObj(superClass.prototype)
    // 修正因为重写子类原型导致子类的constructor属性被修改
    proto.constructor = subClass
    // 设置子类的原型
    subClass.prototype = proto
}
// 测试
// 父类
function SuperClass (name) {
    this.name = name
    this.list = [1, 2, 3]
}
// 父类的原型方法
SuperClass.prototype.getList = function () {
    return this.list
}
// 子类
function SubClass (name, size) {
    // 构造函数继承
    SuperClass.call(this, name)
    // 新增子类
    this.size = size
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass)
// 子新增原型方法
SubClass.prototype.getSize = function () {
    return this.size
}
// 创建实例
const instance1 = new SubClass('instance1', 10)
const instance2 = new SubClass('instance2', 11)
// 修改父类引用类型的公有属性
instance2.list.push(55)
console.log(instance1.name)
console.log(instance2.name)
console.log(instance1.getList()) // [1, 2, 3]
console.log(instance2.getList()) // [1, 2, 3, 55]
console.log(instance1.getSize()) // 10
console.log(instance2.getSize()) // 11

寄生组合式继承利用寄生式继承+构造函数继承,解决了类式继承父类引用类型公共属性被修改影响到其他实例的问题,也解决了父类构造函数重复调用的问题。

多继承

一个常见问题:对于父类A和父类B,可不可以实现一个子类C同时继承A和B

先说结论:

  • js实质上原型链是单一的,所以不存在平级的极继承A又继承B
  • 不论是extend关键字还是mix方法,其本质都是对父级原型方法的浅拷贝(instanceof为false)
  • 除非父类A和B有继承关系,否则子类C不可能同时继承A和B
// 父类 1
function Mother (motherName) {
  this.motherName = motherName
}
Mother.prototype.cook = function () {
  console.log('my mother ' + this.motherName + ' is cooking')
}

// 父类 2
function Father (fatherName) {
  this.fatherName = fatherName
}

Mother.prototype.work = function () {
  console.log('my father ' + this.fatherName + ' is working')
}

// 子类
function Child (name, motherName, fatherName) {
  // 子类调用父类 1的构造函数,继承父类 1属性
  Mother.call(this, motherName)
  // 子类调用父类 2的构造函数,继承父类 2属性
  Father.call(this, fatherName)
  this.name = name
}

// 子类继承父类 2的原型方法
for (const key in Father.prototype) {
  if (!Child.prototype.hasOwnProperty(key)) {
    Child.prototype[key] = Father.prototype[key]
  }
}

// 子类继承父类 1的原型方法
for (const key in Mother.prototype) {
  if (!Child.prototype.hasOwnProperty(key)) {
    Child.prototype[key] = Mother.prototype[key]
  }
}

// 子类自己的原型方法
Child.prototype.learning = function () {
  console.log(this.name + ' is learning')
}

const child = new Child('小红', '小红的妈妈', '小红的爸爸')

console.log(child)
console.log(child.motherName) // '小红的妈妈'
console.log(child.fatherName) // '小红的爸爸'
child.cook() // my mother 小红的妈妈 is cooking
child.work() // my father 小红的爸爸 is working
child.learning() // 小红 is learning
console.log(child instanceof Mother) // false
console.log(child instanceof Father) // false

由上例可知,继承父类的属性可以做到多继承,原型方法可以做到复制,但是受原型链的单一性限制,不能同时继承多个父类的原型

mix多继承(属性复制方法)简写

function mixs () {
    let i = 1,
        len = arguments.length,
        target = arguments[0], // 目标对象
        arg // 缓存参数对象
    for (; i < len; i++) {
        // 缓存当前对象
        arg = arguments[i]
        // 遍历属性并复制赋值
        for (const key in arg) {
            target[key] = arg[key]
        }
    }
    return target
}

const a = {
    name: 'a',
    age: 11
}

const b = {
    name: 'b',
    size: 10
}

const c = mixs({}, a, b)
console.log(c) // {name: "b", age: 11, size: 10}

你可能感兴趣的:(【重学前端】JavaScript中的继承)