【前端面试之JS】js如何实现继承

继承(inheritance)是面向对象软件技术当中的一个概念。
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”
在js中可以用extend来去实现

1、js中常见的继承方式

  • 原型链继承
  • 构造函数继承(借助call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

2、原型链继承

这个是用extend实现的继承。可以看出new的两个对象,彼此之间的值,没有任何的影响

class Car {
    constructor(color, speed) {
        this.color = color
        this.speed = [1, 2, 3, 4]
        // ...
    }
}
// // 货车
class Truck extends Car {
    constructor(color, speed) {
        super(color, speed)
        this.Container = true // 货箱
    }
}
let dadi = new Truck('白色')
let heid = new Truck('红色')
dadi.speed.push(5)
console.log(dadi.speed)
console.log(heid.speed)
 console.log(Truck.prototype)

输出:
在这里插入图片描述
然而采用原型链实现继承的话,就是将父类直接赋值给子类的prototype对象上

function Parent() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
}
function Child() {
    this.type = 'child2';
}
Child.prototype = new Parent();
var s1 = new Child();
var s2 = new Child();
s1.name = 's1';
s1.play.push(4);
console.log(s1.play);
console.log(s2.play);

console.log(s1.name);
console.log(s2.name);

在这里插入图片描述
在这里插入图片描述

这里其实可以看出来,当同时new出两个对象。
其中一个修改了play,另外一个对象的play同时也发生改变了。
这个是因为采用直接赋值的方式,相当于浅拷贝
父类中若是存在引用类型的话,只能复制对应的地址,因此修改的时候才会发生改变。
这里当修改name的时候,不发生改变。因为name是字符串,是原始数据类型。相当于又复制了一个出来,互不影响。
**采用extend实现的继承,就不存在这个问题。**因此这个方法不是最优的。

3、构造函数继承

借助call,Parent.call(this)改变Parent中的this指向。使得Child可以访问到Parent中的属性。

function Parent() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
}

Parent.prototype.getName = function () {
    return this.name;
}

function Child() {
    Parent.call(this);
    this.type = 'child'
}

let child1 = new Child();
let child2 = new Child();
child1.play.push(4);
console.log(child1.play);  // 1,2,3,4
console.log(child2.play);  // 1,2,3
console.log(child1.getName())// 报错:child1.getName is not a function

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法。
为什么不能原型的属性以及方法呢?因为这里的原型指向已经丢失了

在Parent构造函数中输出this,可以发现这里的prototype指向的是Object。

在这里插入图片描述

4、组合继承

集合了上面两种方法

function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
    return this.name;
}
function Child3() {
    // 第二次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

方式一和方式二的问题都解决了,但是这里Parent执行两次,造成了资源的浪费。
在这里插入图片描述

原型式继承(借助Object.create方法)

let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };

  let person4 = Object.create(parent4);
  person4.name = "tom";
  person4.friends.push("jerry");

  let person5 = Object.create(parent4);
  person5.friends.push("lucy");

  console.log(person4); // tom
  console.log(person4.name === person4.getName()); // true
  console.log(person5.name); // parent4
  console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
  console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]

在这里插入图片描述

Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。出现的问题其实是跟原型链直接复制出现的错误是一样的。

寄生式继承

缺点跟原型式继承一样。

let parent5 = {
     name: "parent5",
     friends: ["p1", "p2", "p3"],
     getName: function () {
         return this.name;
     }
 };

 function clone(original) {
     let clone = Object.create(original);
     clone.getFriends = function () {
         return this.friends;
     };
     return clone;
 }

 let person5 = clone(parent5);
 let person6 = clone(parent5);
 console.log(person5); // parent5
 console.log(person5.getFriends()); // ["p1", "p2", "p3"]
 person5.friends.push(2);
 console.log(person6.getFriends());

在这里插入图片描述

寄生组合式继承

是目前来说最优的继承方式:

function clone (parent, child) {
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
}

function Parent6() {
    this.name = 'parent6';
    this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
    return this.name;
}
function Child6() {
    Parent6.call(this);
    this.friends = 'child5';
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function () {
    return this.friends;
}
let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

在这里插入图片描述

总结

  • 原型链继承:new父类,并赋值给子类的原型属性
    • 缺点:new出来的对象,其中一个修改引用数据类型,另外一个会跟着变化。 因为两个实例使用的是同一个原型对象,内存空间是共享的。
  • 构造函数继承:借助call调用parent函数。
    • 缺点:相较于第一种原型链继承方式,父类的引用属性不会被共享。
  • 组合继承:组合了前两种方式
    • 缺点:person执行了两次,造成了多构造一次的性能开销。
  • 原型式继承:借助Object.create方式实现普通对象的继承
    • 缺点: 因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。
  • 寄生式继承。缺点跟原型式继承一样。
  • 寄生组合式继承 :采用Object.create+call方式(最优)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值