共计 5351 个字符,预计需要花费 14 分钟才能阅读完成。
提到 JS 继承,你首先想到的什么?面试
继承形式
优缺点...
,js 作为已经的苦主,我看了忘,忘了看,看了又忘,算了,放弃医治还不行么 …… 辽阔的大草原上,万匹草泥马在奔流,都 9012 年了面试官还不放过我。
ok,不扯皮了,言归正传,来聊聊 js 继承这个经典的话题。
JS 的“类”
javascript 不像 java,php 等传统的 OOP 语言,js 自身并没有类这个概念,那么它是怎么实现类的模仿呢?
- 构造函数形式
- 原型形式
- 混合形式
构造函数形式
Function Foo (name) {
this.name = name
this.like = function () {console.log(`like${this.name}`)
}
}
let foo = new Foo('bibidong')
像这样就是通过构造函数的形式来定义类,其实和一般函数一样,但为了和惯例函数有个辨别,个别把函数名首字母大写。
- 毛病:无奈共享类的办法。
原型形式
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,都能够来创建对象,定义类。
常见的继承形式
一、原型链继承
基于原型链查找的特点,咱们将父类的实例作为子类的原型,这种继承形式便是原型链继承。
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 属性举个例子,相当于就是这样
Child.prototype.color = 'red'
这样父类的实例属性都被共享了,咱们打印一下 child,能够看到 child 没有本人的实例属性,它拜访的是它的原型对象。
咱们创立两个实例 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 属性被实例批改,这样必定就影响到了所有实例。
-
毛病
- 子类的实例会共享父类构造函数援用类型的属性
- 创立子类实例的时候无奈传参
二、构造函数式继承
相当于拷贝父类的实例属性给子类,加强了子类构造函数的能力
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__
下面没有父类的原型对象。解决了原型链的两个问题(子类实例的各个属性互相独立、还能传参)
-
毛病
- 子类无奈继承父类原型下面的办法和属性。
- 在构造函数中定义的办法,每次创立实例都会再创立一遍。
三、组合继承
人如其名,组合组合,肯定把什么货色组合起来。没错,组合继承便是把下面两种继承形式进行组合。
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 也执行了一次父类构造函数
四、原型式继承
将一个对象作为根底,通过解决失去一个新对象,这个新对象会将原来那个对象作为原型,这种继承形式便是原型式继承,一句话总结就是将传入的对象作为要创立的新对象的原型。
先写下这个有解决能力的函数
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,和原型链继承相似,这种继承形式下实例没有本人的属性值,拜访到也是原型上的属性。
- 毛病:同原型链继承
五、寄生式继承
原型式继承的降级,寄生继承封装了一个函数,在外部加强了原型式继承产生的对象。
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,它的毛病也很显著了,寄生式继承加强了对象,却也无奈防止原型链继承的问题。
-
毛病
- 领有原型链继承的毛病
- 除此,外部的函数无奈复用
六、寄生组合式继承
大招来了,寄生组合闪亮退场!
下面说到,组合继承的问题在于会调用二次父类,造成子类原型上产生多余的同名属性。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 作为原型的新对象,这不就是下面原型式继承的处理函数 prodObject
吗
Child.prototype = Object.create(Parent.prototype) // 改为这样
这样就解决了所有问题,咱们怕改写 Child.prototype
影响父类,通过 Object.create
返回的实例对象,咱们将 Child.prototype
间接指向 Parent.prototype
,当再减少addByChild
办法时,属性就和父类没关系了。
寄生组合式继承也被认为是最完满的继承形式,最举荐应用。
总结
js 的继承形式次要就这六种,es6 的继承是个语法糖,实质也是基于寄生组合。这六种继承形式,其中原型链继承和构造函数继承最为根底和经典,组合式继承聚合了它们二者的能力,性能是没问题的。原型式继承和原型链类似,寄生式继承是在原型式继承根底上变动而来,它加强了原型式继承的能力。最初的寄生组合继承解决了组合继承的问题,是一种最为现实的继承形式。
明天七夕,在线乞讨,不要女朋友只有赞,溜了溜了~