面试官: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,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

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

相关推荐
翻滚吧键盘28 分钟前
js代码09
开发语言·javascript·ecmascript
万少1 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL1 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl021 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang1 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景1 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼1 小时前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿1 小时前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再1 小时前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
jingling5552 小时前
面试版-前端开发核心知识
开发语言·前端·javascript·vue.js·面试·前端框架