夯基础-手撕js继承

提到JS继承,你首先想到的什么? 面试 继承方式 优缺点...,js继承做为曾经的苦主,我看了忘,忘了看,看了又忘,OMG,都9012年了面试官还不放过我。javascript

ok,开开玩笑,接下来言归正传,来聊聊js继承这个经典的话题。php

JS的“类”

javascript不像java,php等传统的OOP语言,js自己并无类这个概念,那么它是怎么实现类的模拟呢?java

  1. 构造函数方式
  2. 原型方式
  3. 混合方式

构造函数方式

Function Foo (name) {
    this.name = name
    this.like = function () {
        console.log(`like${this.name}`)
    }
}
let foo = new Foo('bibidong')

像这样就是经过构造函数的方式来定义类,其实和普通函数同样,但为了和常规函数有个区分,通常把函数名首字母大写。es6

  • 缺点:没法共享类的方法。

原型方式

function Foo (name) {}
Foo.prototype.color = 'red'
Foo.prototype.queue = [1,2,3]
let foo1 = new Foo()
let foo2 = new Foo()

foo1.queue.push(4)
console.log(foo1)   // [1, 2, 3, 4]
console.log(foo2)   // [1, 2, 3, 4]

咱们经过原型方式直接把属性和方法定义在了构造函数的原型对象上,实例能够共享这些属性和方法,解决了构造函数方式定义类的缺点。面试

  • 缺点:能够看到咱们改变了foo1的数据,结果foo2的queue属性也变了,这即是原型方式最大的问题,引用类型的属性会被其它实例修改。除此以外,这种方式下也没法传参。

混合方式

function Foo (name) {   // 属性定义在构造函数里面
    this.name = name
    this.color = 'red'
    this.queue = [1,2,3]
}
Foo.prototype.like = function () {  // 方法定义在原型上
    console.log(`like${this.name}`)
}
let foo1 = new Foo()
let foo2 = new Foo()

所谓混合模式,即是把上面两种方式混合起来,咱们在构造函数里面定义属性,在原型对象上定义要共享的方法,既能传参,也避免了原型模式的问题。函数

小结一下:js类的能力是模拟出来的,能够经过构造函数方式,原型方式来定义,混合模式则聚合了前二者的优势。除此,还有Object.create(), es6的class,均可以来建立对象,定义类。this

常见的继承方式

1、原型链继承

基于原型链查找的特色,咱们将父类的实例做为子类的原型,这种继承方式即是原型链继承。spa

function Parent () {
    this.color = 'red'
    this.queue = [1,2,3]
}
Parent.prototype.like = function () {
    console.log('')
}

function Child () { }
Child.prototype = new Parent()  // constructor指针变了 指向了Parent
Child.prototype.constructor = Child     // 手动修复

let child = new Child()

Child.prototype至关因而父类Parent的实例,父类Parent的实例属性被挂到了子类的原型对象上面,拿color属性举个例子,至关于就是这样prototype

Child.prototype.color = 'red'

这样父类的实例属性都被共享了,咱们打印一下child,能够看到child没有本身的实例属性,它访问的是它的原型对象。3d

咱们建立两个实例child1,child2

let child1 = new Child()
let child2 = new Child()
child1.color = 'bulr'
console.log(child1)
console.log(child2)

咱们修改了child1的color属性,child2没有受到影响,并不是是其它实例拥有独立的color属性,而是由于这个color属性直接添加到了child1上面,它原型上的color并无动,因此其它实例不会受到影响从打印结果也能够清楚看到这一点。那若是咱们修改的属性是个引用类型呢?

child1.queue = [1,2,3,'我被修改了'] // 从新赋值
child1.like = function () {console.log('like方法被我修改了')}
console.log(child1)
console.log(child2)

咱们重写了引用类型的queue属性和like方法,其实和修改color属性是彻底同样的,它们都直接添加到了child1的实例属性上。从打印结果能看到这两个属性已经添加到了child1上了,而child2并不会受到影响,再来看下面这个。

child1.queue.push('add push')   // 此次没有从新赋值
console.log(child1)
console.log(child2)

若是进行了从新赋值,会添加到到实例属性上,和原型上到同名属性便无关了,因此并不会影响到原型。此次咱们采用push方法,没有开辟新空间,修改的就是原型。child2的queue属性变化了,子类Child原型上的queue属性被实例修改,这样确定就影响到了全部实例。

  • 缺点

    • 子类的实例会共享父类构造函数引用类型的属性
    • 建立子类实例的时候没法传参

2、构造函数式继承

至关于拷贝父类的实例属性给子类,加强了子类构造函数的能力

function Parent (name) {
    this.name = name
    this.queue = [1,2,3]
}
Parent.prototype.like = function () {
    console.log(`like${this.name}`)
}

function Child (name) {
    Parent.call(this, name)    // 核心代码
}

let child = new Child(1)

咱们打印了一下child,能够看到子类拥有父类的实例属性和方法,可是child的__proto__上面没有父类的原型对象。解决了原型链的两个问题(子类实例的各个属性相互独立、还能传参)

  • 缺点

    • 子类没法继承父类原型上面的方法和属性。
    • 在构造函数中定义的方法,每次建立实例都会再建立一遍。

3、组合继承

人如其名,组合组合,必定把什么东西组合起来。没错,组合继承即是把上面两种继承方式进行组合。

function Parent (name) {
    this.name = name
    this.queue = [1,2,3]
}
Parent.prototype.like = function () {
    console.log(`like${this.name}`)
}

function Child (name) {
    Parent.call(this, name)
}

Child.prototype = new Parent()
Child.prototype.constructor = Child     // 修复constructor指针
let child = new Child('')

接下来咱们作点什么,看它组合后能不能把原型链继承和构造函数继承的优势发扬光大

let child1 = new Child('bibidong')
let child2 = new Child('huliena')
child1.queue.push('add push')
console.log(child1)
console.log(child2)

咱们更新了child1的引用属性,发现child2实例没受到影响,原型上的like方法也在,不错,组合继承确实将两者的优势发扬光大了,解决了两者的缺点。组合模式下,一般在构造函数上定义实例属性,在原型对象上定义要共享的方法,经过原型链继承方法让子类继承父类构造函数原型上的方法,经过构造函数继承方法子类得以继承构造函数的实例属性,是一种功能上较完美的继承方式。

  • 缺点:父类构造函数被调用了两次,第一次调用后,子类的原型上拥有了父类的实例属性,第二次call调用复制了一份父类的实例属性做为子类Child的实例属性,那么子类原型上的同名属性就被覆盖了。虽然被覆盖了功能上没什么大问题,但这份多余的同名属性一直存在子类原型上,若是咱们删除实例上的这个属性,实际上还能访问到,此时获取到的是它原型上的属性。
Child.prototype = new Parent() // 第一次构建原型链
Parent.call(this, name) // 第二次new操做符内部经过call也执行了一次父类构造函数

4、原型式继承

将一个对象做为基础,通过处理获得一个新对象,这个新对象会将原来那个对象做为原型,这种继承方式即是原型式继承,一句话总结就是将传入的对象做为要建立的新对象的原型。

先写下这个有处理能力的函数

function prodObject (obj) {
    function F (){
        
    }
    F.prototype = obj
    return new F()  // 返回一个实例对象
}

这也是Object.create()的实现原理,因此用Object.create直接替换prodObject函数是ok的
let base = {
    color: 'red',
    queue: [1, 2, 3]
}
let child1 = prodObject(base)
let child2 = prodObject(base)
console.log(child1)
console.log(child2)

原型式继承基于prototype,和原型链继承相似,这种继承方式下实例没有本身的属性值,访问到也是原型上的属性。

  • 缺点:同原型链继承

5、寄生式继承

原型式继承的升级,寄生继承封装了一个函数,在内部加强了原型式继承产生的对象。

function greaterObject (obj) {
    let clone = prodObject(obj)
    clone.queue = [1, 2, 3]
    clone.like = function () {}
    return clone
}
let parent = {
    name: 'bibidong',
    color: ['red', 'bule', 'black']
}
let child = greaterObject(parent)

打印了一下child,它的缺点也很明显了,寄生式继承加强了对象,却也没法避免原型链继承的问题。

  • 缺点

    • 拥有原型链继承的缺点
    • 除此,内部的函数没法复用

6、寄生组合式继承

大招来了,寄生组合闪亮登场!

上面说到,组合继承的问题在于会调用二次父类,形成子类原型上产生多余的同名属性。Child.prototype = new Parent(),那这行代码该怎么改造呢?

咱们的目的是要让父类的实例属性不出如今子类原型上,若是让Child.prototype = Parent.prototype,这样不就能保证子类只挂载父类原型上的方法,实例属性不就没了吗,代码以下,看起来好像是简直不要太妙啊。

function Parent (name) {
    this.name = name
    this.queue = [1,2,3]
}
Parent.prototype.like = function () {
    console.log(`like${this.name}`)
}

function Child (name) {
    Parent.call(this, name)
}

Child.prototype = Parent.prototype // 只改写了这一行
Child.prototype.constructor = Child
let child = new Child('')

回过神忽然发现改写的那一行若是Child.prototype改变了,那岂不是直接影响到了父类,举个栗子

Child.prototype.addByChild = function () {}
Parent.prototype.hasOwnProperty('addByChild')   // true

addByChild方法也被加到了父类的原型上,因此这种方法不够优雅。一样仍是那一行,直接访问到Parent.prototype存在问题,那咱们能够产生一个以Parent.prototype做为原型的新对象,这不就是上面原型式继承的处理函数prodObject

Child.prototype = Object.create(Parent.prototype) // 改成这样

这样就解决了全部问题,咱们怕改写Child.prototype影响父类,经过Object.create返回的实例对象,咱们将Child.prototype间接指向Parent.prototype,当再增长addByChild方法时,属性就和父类不要紧了。

寄生组合式继承也被认为是最完美的继承方式,最推荐使用。

总结

js的继承方式主要就这六种,es6的继承是个语法糖,本质也是基于寄生组合。这六种继承方式,其中原型链继承和构造函数继承最为基础和经典,组合继承聚合了它们两者的能力,但在某些状况下会形成错误。原型式继承和原型链类似,寄生式继承是在原型式继承基础上变化而来,它加强了原型式继承的能力。最后的寄生组合继承解决了组合继承的问题,是一种最为理想的继承方式。


今天七夕,在线乞讨,不要女友只要赞,溜了溜了~

相关文章
相关标签/搜索