面试官:js如何实现继承

前言

继承在js中算是比较烦人的东西,因为方法比较多,本期文章就带你挨个理清

继承实现的效果就是一个函数 (或对象) 可以从别的函数 (或对象) 那里继承到属性,比如下面,我可以打印出Tom才能认为继承到了

javascript 复制代码
function Parent() {
    this.name = 'Tom'
}

function Child() {
    this.age = 18
}

let child = new Child()

console.log(child.name); // undefined

原型链继承

child实例对象寻找这个name属性,会先去构造函数的显示具有的属性去找,发现没有后会去自己的隐式原型找,实例对象的隐式原型就是构造函数的显示原型,因此就会去Childprototype身上找,好了,已经有想法了,直接把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);

这种写法应该是非常合乎逻辑的,实例的时候传参,但是这个继承方法不行

缺点

  1. 多个实例对象共用了同一个原型,属性会被修改
  2. 子类无法给父类传参

因此这个方法适用于确定自己不会去修改原型上的属性,并且不需要传参

构造函数继承

这个方法会比较巧妙,既然想要子类需要继承到父类的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

也可以理解,ChildParent的原型这样写是没有半毛钱关系,因此这个方法继承父类继承得不够彻底

缺点

  1. 无法继承父类原型上的属性

因此这个方法适用于父类没有原型的情况

组合继承(经典继承)

原型链继承可以解决继承原型的问题,构造函数继承可以解决原型问题,二者结合就是组合继承

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混乱了

我们清楚,实例对象pconstructor属性就是构造函数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

缺点

  1. 父类被调用两次

原型式继承

继承其实也可以发生在对象身上,就是Object.create()

这个方法我们以前在浅拷贝中用到过,他拷贝的对象,没有属性,其属性全部拷贝到了原型身上去了,也可以说继承到原型上去了

同样,因为是原型继承,多个子类同样可以修改父类的应用类型的值

缺点

  1. 多个实例对象共用了同一个原型,属性会被修改

寄生组合继承

其实寄生组合继承就是用来优化组合继承的两次调用父类这个缺点

既然要去掉一个调用父类,那么一定是去掉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一样的,extendssuper一起使用,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

其实es6class继承就是用继承组合继承来打造的,childconstructor依旧是指向Child,没有缺陷

最后

这个继承问题很容易被面试官问到,希望看完本期文章,关于继承问题就再也不怕面试官如何问你了

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!

相关推荐
Cshaosun8 分钟前
js版本之ES5特性简述【String、Function、JSON、其他】(二)
前端·javascript·es
__WanG11 分钟前
Flutter将应用打包发布到App Store
前端·flutter·ios
leluckys14 分钟前
flutter 专题十七 Flutter Flar动画实战
前端·flutter
豆包MarsCode30 分钟前
我用豆包MarsCode IDE 做了一个 CSS 权重小组件
开发语言·前端·javascript·css·ide·html
22x艾克斯39 分钟前
Web Notifications API-让网页也能像QQ一样实现消息通知
前端
X 西安41 分钟前
第十章JavaScript的应用
开发语言·javascript·ecmascript
想你的风吹到了瑞士1 小时前
变量提升&函数提升
前端·javascript·vue.js
生椰拿铁You1 小时前
12 —— Webpack中向前端注入环境变量
前端
Huazzi.1 小时前
免费好用的静态网页托管平台全面对比介绍
前端·网络·github·web
吃土少女古拉拉2 小时前
前端和后端
前端·学习笔记