【重学前端】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}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352