前言
继承在js中算是比较烦人的东西,因为方法比较多,本期文章就带你挨个理清
继承实现的效果就是一个函数 (或对象) 可以从别的函数 (或对象) 那里继承到属性,比如下面,我可以打印出Tom
才能认为继承到了
javascript
function Parent() {
this.name = 'Tom'
}
function Child() {
this.age = 18
}
let child = new Child()
console.log(child.name); // undefined
原型链继承
child
实例对象寻找这个name
属性,会先去构造函数的显示具有的属性去找,发现没有后会去自己的隐式原型找,实例对象的隐式原型就是构造函数的显示原型,因此就会去Child
的prototype
身上找,好了,已经有想法了,直接把Parent
的实例赋给Child
的显示原型上去,如下
javascript
function Parent() {
this.name = 'Tom'
}
Child.prototype = new Parent()
function Child() {
this.age = 18
}
let child = new Child()
console.log(child.name); // Tom
下面我们再来看下这个方法的缺点
首先我们要清楚,实例对象如果想要更改构造函数的属性,他只能改动引用类型的value值,原始类型改不动
比如下面,我想要改name
属性,其实child自身是没有name
属性,构造函数的name
属性挂在它的原型身上,因此最终的效果是给child
自身添加了name
属性,没有改动原型身上的name
我们再看下,我更改引用类型的value
呢?
因此,这就是个缺陷,既然原型身上的东西都被改动了,如果我再去new
一个实例对象,那么他继承到的属性就是被别人改动后的属性
javascript
function Parent() {
this.name = 'Tom'
this.girlfriend = {
count: 1
}
}
Child.prototype = new Parent()
function Child() {
this.age = 18
}
let child = new Child()
child.girlfriend.count = 0
let child2 = new Child()
console.log(child.girlfriend); // { count: 0 }
console.log(child2.girlfriend); // { count: 0 }
因此这个方法继承有个缺点,不适合多个实例对象,被继承构造函数的属性若是引用类型可以被任意实例对象修改
另外还有个问题,子类不能给父类传参,如下
javascript
function Parent() {
this.name = 'Tom'
}
Child.prototype = new Parent()
function Child(age) {
this.age = age
}
let child = new Child(18, 'John') // 实现不了
console.log(child);
这种写法应该是非常合乎逻辑的,实例的时候传参,但是这个继承方法不行
缺点
- 多个实例对象共用了同一个原型,属性会被修改
- 子类无法给父类传参
因此这个方法适用于确定自己不会去修改原型上的属性,并且不需要传参
构造函数继承
这个方法会比较巧妙,既然想要子类需要继承到父类的name
属性,也就是说把this.name = 'Tom'
这段代码搬到Child
中来,也就是说,让父类的this
指向子类,我们可以用call
显示绑定this
的指向,如下
javascript
function Parent() {
this.name = 'Tom'
}
function Child() {
Parent.call(this) // this.name = 'Tom'
this.age = 18
}
let c1 = new Child()
console.log(c1.name); // Tom
那这个方法有没有原型链继承方法的两个缺点呢?
首先,多个实例对象的话,this
是各自指向各自,已经没有公用的原型了,因此第一个缺点没有;第二个缺点得话也没有,可以让子类给父类传参,如下
javascript
function Parent(name) {
this.name = name
}
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let c1 = new Child('Tom')
console.log(c1.name); // Tom
目前来看,这个方法非常完美,实际上这个方法有个致命的缺点,它无法继承父类构造函数的属性,比如下面这样
javascript
Parent.prototype.getName = function () {
return this.name
}
function Parent(name) {
this.name = name
}
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let c1 = new Child('Tom')
console.log(c1.getName()); // TypeError: c1.getName is not a function
也可以理解,Child
和Parent
的原型这样写是没有半毛钱关系,因此这个方法继承父类继承得不够彻底
缺点
- 无法继承父类原型上的属性
因此这个方法适用于父类没有原型的情况
组合继承(经典继承)
原型链继承可以解决继承原型的问题,构造函数继承可以解决原型问题,二者结合就是组合继承
javascript
Parent.prototype.getName = function () {
return this.name
}
function Parent(name) {
this.name = name
}
Child.prototype = new Parent()
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let child = new Child('John')
console.log(child.getName()); // John
其实这个方法也有个问题,就是constructor
混乱了
我们清楚,实例对象p
的constructor
属性就是构造函数Parent
,谁创建我constructor
就是谁
我们现在看下这个方法下的子类的constructor
指向谁
child
实例居然由Parent
创建的,而不是Child
......
正常来说,实例child
的原型里应该有constructor
,但是没了,这是因为Child.prototype = new Parent()
直接将Child
的原型修改了,全部赋值成了Parent
,这个问题暴露出来也情有可原,其实constructor
属性是给v8
用的,因此这么一来可能会导致一些潜在的bug
当然,这个问题也非常好解决,直接把constructor
给人家加上
javascript
Parent.prototype.getName = function () {
return this.name
}
function Parent(name) {
this.name = name
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let child = new Child('John')
console.log(child.getName()); // John
现在就没有缺点了,这也就是为什么组合继承被称之为经典继承,因为非常好用
但是!有人鸡蛋中挑骨头,说这个继承有两次调用父类,一个new Parent
,还有个Parent.call
缺点
- 父类被调用两次
原型式继承
继承其实也可以发生在对象身上,就是Object.create()
这个方法我们以前在浅拷贝中用到过,他拷贝的对象,没有属性,其属性全部拷贝到了原型身上去了,也可以说继承到原型上去了
同样,因为是原型继承,多个子类同样可以修改父类的应用类型的值
缺点
- 多个实例对象共用了同一个原型,属性会被修改
寄生组合继承
其实寄生组合继承就是用来优化组合继承的两次调用父类这个缺点
既然要去掉一个调用父类,那么一定是去掉new Parent
,但是这样父类的原型就无法给到子类了,那就直接利用对象的继承Object.create
来实现,将父类的原型传入进去,赋值给子类,这样就实现了让子类继承到父类的原型,同时不用多次调用父类
javascript
Parent.prototype.getName = function () {
return this.name
}
function Parent(name) {
this.name = name
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
function Child(name) {
Parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let child = new Child('John')
console.log(child.getName()); // John
这个方法就没有缺陷了
class继承
只要让子类也能打印出Tom
,就代表继承成功
javascript
class Parent {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child {
constructor() {
this.age = 18
}
}
let child = new Child('Tom')
console.log(child.name); // undefined
语法跟Java
一样的,extends
和super
一起使用,super
用于子类向父类传参,不过super
要写上面去
scala
class Parent {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
class Child extends Parent{
constructor(name) {
super(name) // super让子类传参给父类
this.age = 18
}
}
let child = new Child('Tom')
console.log(child.name); // Tom
其实es6
的class
继承就是用继承组合继承来打造的,child
的constructor
依旧是指向Child
,没有缺陷
最后
这个继承问题很容易被面试官问到,希望看完本期文章,关于继承问题就再也不怕面试官如何问你了
如果你对春招感兴趣,可以加我的个人微信:
Dolphin_Fung
,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决
另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!