面试官:除了call,apply你还了解其他JS实现继承的方式吗?

在之前的面试过程中,经常会被面试官问到JS实现继承的方式。由于在校期间修过Java,日常开发中常用的是ES6的extends所以一直没有对JS的继承深入了解,知道看了《JavaScript设计模式》这本书后,才发现JS中的继承,其实大有学问,接下来我就借花献佛,给大家简单分享一下JS的继承方式。

在了解各种继承之前,需要大家了解创建一个类的实例var book1 = new classA()中发生了什么。

  1. 创建一个空的新对象:
var obj = {};
  1. 设置新对象的__proto__属性指向类的prototype属性
obj.__proto__ = classA.prototype;
  1. 使用新对象调用类的构造函数,并且将this指向新对象
classA.call(obj);
  1. 将初始化完毕的新对象(obj)地址保存到等号左边,完毕
var book1 = new classA();

一句new的背后到底隐藏着什么秘密?

1 类式继承

// 父类
function SuperClass() {
  // 共有方法
  this.superValue = true;
}

// 父类添加原型方法
SuperClass.prototype.getSuperValue = function() {
  return this.superValue;
}

// 子类
function SubClass() {
  this.subValue = false;
}

// 类式继承
SubClass.prototype = new SuperClass();

SubClass.prototype.getSubValue = function() {
  return this.subValue;
}

var sub1 = new SubClass();

我们看到SubClass.prototype = new SuperClass()这句话。类的原型对象就是为类的原型添加共有方法,我们实例化一个父类时(想想文章开头new一个对象的四个步骤),新对象将__proto__指向了父对象的prototype属性(第二步),并且复制了父类的构造函数内的属性和方法(第三步),创建的对象不仅可以访问父类原型上的属性和方法,也可以访问父类构造函数中的属性和方法(就是prototype中的方法和SuperClass中的方法)。

类式继承的缺点

1. 如果父类构造函数中的共有属性有引用类型,当我们修改某个实例的这个引用类型的时候,另一个实例也会受影响
2. 子类在创建的时候,无法给父类的构造函数传参(相当于每次从父类继承过来的属性都是同一个)

2 构造函数继承

为了解决类式继承的两个缺点,我们可以尝试着使用构造函数继承(但这种方法也有缺陷,要不然文章到这不就完结了嘛? )

function SuperClass(id) {
  // 引用类型共有属性
  this.books = [1, 2, 3];
  // 值类型共有属性
  this.id = id;
}

SuperClass.prototype.showBooks = function() {
  console.log(this.books);
};

function SubClass(id) {
  SuperClass.call(this, id); // 构造函数继承,对没错是你最熟悉的样子
}

var instance1 = new SubClass(10);
var instance2 = new SubClass(11);
instance1.books.push(4);
console.log(instance1.books, instance1.id, instance2.book, instance2.id); // [1, 2, 3] 10 [1, 2, 3, 4] 11

instance1.showBooks(); // TypeError

注意!注意!SuperClass.call(this, id) 是构造函数继承的精华,由于call可以改变this的指向,所以等于在子类的构造函数中将父类的构造函数执行了一遍,因此解决了类式继承中无法传参(现在可以在子类的构造函数中传参给父类的构造函数)和多个实例中修改引用类型属性会同时影响(每次创建一个实例,都会将构造函数重新执行一遍)的缺点。

But!But! 由于这种继承不涉及到原型prototype,所以父类的原型属性我们就继承不了了,可以看到代码最后报了’TypeError’的错误。这也是构造函数继承的一个缺点,那有不足的地方就得改正啊,怎么改正呢?组合继承了解一下?

3 组合继承

其实我们大胆一点,可以把类式继承构造函数继承看做是两种基础的继承类型,而组合继承就是类式继承 + 构造函数继承

function SuperClass(name) {
  this.name = name;
  this.books = [1, 2, 3];
}

SuperClass.prototype.getName = function() {
  console.log(this.name);
}

function SubClass(name, time) {
  SuperClass.call(this, name);  // 构造函数继承方法
  // 从父类继承后,新增一个属性
  this.time = time;
}

SubClass.prototype = new SuperClass(); // 类式继承方法

SubClass.prototype.getTime = function() {
  console.log(this.time);
}

var instance1 = new SubClass('name1', 2020);
var instance2 = new SubClass('name2', 2020);

instance1.books.push(4);
console.log(instance1.books); // [1, 2, 3]
console.log(instance2.books); // [1, 2, 3, 4] 改变引用类型属性不会对其他实例造成影响
instance1.getName(); // 可以调用父类的原型属性
instance1.getTime(); // 子类的原型属性
instance2.getName();
instance2.getTime();

OK! Nice! 我们可以看到,使用了组合继承,既解决了类式继承的引用类型问题和传参问题(类式继承的缺点),又解决了构造函数继承不能使用父类的原型属性问题(构造函数继承的缺点)。

BUT! BUT 有没有发现我们父类的构造函数在构造函数继承中被调用了一次,在类式继承中又被调用了一次(如果不清楚为什么,可以参考new一个对象发生了什么)。没错,我们使用组合继承,父类构造函数被调用了两次,所以这并不是最完美的方式。

keep patient ! keep patient!

可以先提前透露下,最优秀的继承方式:·寄生组合式继承,但是在这之前需要先了解一下原型式继承和寄生式继承。

4 原型式继承

function inheritObject(o) {
  // 声明一个过渡函数对象
  function F() {}
  // 过渡对象的原型继承父对象 (感觉和类式继承有点像)
  F.prototype = o;
  return new F();
}

对于这个原型式继承,其实是基于类式继承的一个封装。类式继承需要我们先声明一个子类,然后将子类的prototype指向父元素的实例,而原型式继承则是在内部声明一个过渡对象,直接返回实例。(但是我个人感觉,原型式继承不是很灵活,如果我们子类需要额外的属性,就得每次去修改funciotn F()这个过渡对象的构造函数,或者使用其他办法)

5 寄生式继承

啪啪啪!我上面不是才说了原型式继承这种方式不太灵活吗?如果子类想要赋予额外的属性,是一件很麻烦的事情,寄生式继承就提供了一种更好的方案。

function inheritObject(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

var book = {
  name: 'js book',
  aLikeBook: ['css book', 'html book']
}

function createBook(obj) {
  var o = new inheritObject(obj);
  o.getName = function() {
    console.log(name);
  };
  return o;
}

其实吧,寄生式继承也没啥,又是基于原型式继承的二次封装,并且在这第二次封装中对继承的对象进行了拓展,这样创建的对象不仅仅有父类中的属性和方法,还可以添加新的属性和方法。

6 终极继承者–寄生组合式继承

终于,到了最NB的一种继承方式了。顾名思义,应该就是构造函数继承 + 寄生继承(包括了类式继承和原型式继承)
之前说的组合式继承存在一些问题:

  1. 在组合式继承中,存在父类的构造函数被调用两次的问题

  2. 子类不是父类的实例,而子类的原型是父类的实例(书上这样说的,我暂时也没太想明白为什么这样不妥)

组合寄生式集成可以分成两部分:一部分式继承父类的属性和方法,另一部分继承父类的原型属性

  1. 继承父类的属性和方法,我们使用构造函数继承
  2. 通过寄生式继承重新继承父类的原型
// 原型式继承
function inheritObject(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

// 对子类原型的处理,为什么呢?我们不希望子类的原型是父类的实例,听着别扭?你可以打印一下类式继承的sub1.__proto__.constructor 发现是父类的构造函数,接下来需要进行改造
function inheritPrototype(subClass, superClass) {
  // 复制一份父类的原型副本保存在变量中
  var p = inheritObject(superClass.prototype);
  // 修正因为重写子类原型导致子类的constructor属性被修改
  p.constructor = subClass;
  // 设置子类的原型
  subClass.prototype = p; // p可以看作是父类的一个实例,而F只是一个过渡对象
}
// 定义父类
function SuperClass(name) {
    this.name = name;
    this.color = ['red', 'blue', 'green'];
}
// 定义父类的原型方法
SuperClass.prototype.getName = function() {
  console.log(this.name);
}
// 定义子类
function SubClass(name, time) {
  // 构造函数式继承,继承父类的共有属性和方法
  SuperClass.call(this, name);
  // 子类新增属性
  SubClass.time = time;
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass);
// 子类新增原型方法
SubClass.prototype.getTime = function() {
  console.log(this.time);
}
// 创建两个实例来测试一下
var instance1 = new SubClass('js book', 2019);
var instance2 = new SubClass('css book', 2020);

instance1.colors.push("black");
console.log(instance1.colors);
console.log(instance2.colors);
instance2.getName();
instance2.getTime();

首先看到构造函数继承,帮助我们执行了父类的构造函数,继承了父类的共有属性。重点在inheritPrototype这个函数中,由于我们希望子类是父类的实例,于是乎使用inheritObject方法创建一个父类的实例(F相当于只是一个过渡对象),由于我们使用类式继承constructor是父类的构造函数,需要修正一下p.constuctor = subClass,改成自己的构造函数。

现在我们可以看到,instance.proto.constructor已经为子类的构造函数了。(有没有发现,父类的构造函数这时候没有被调用两次,inheritPrototype只是对父类的原型进行了复制)

参考内容: 《JavaScript设计模式第二章》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值