JS继承的实现——原型链

127 阅读5分钟

这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情

前导

  • ES5中的继承使用原型链实现,而ES6由于Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。而且extends 的写法比 ES5 的原型链继承,要清晰和方便很多。这里主要来探讨下原型链,对ES6的继承在之后再探讨。
  • 继承:主要分为接口继承(只继承函数的接口(声明))和实现继承(继承实际的方法),而JS只有实现继承

原型链

基本思想

  • 利用原型将一个引用类型继承另一个引用类型的属性和方法。
  • 由于每个构造函数都有一个原型对象指向该构造函数,同时该实例又会指向这个原型对象。借助这个特性,让一个原型对象等于另一个实例,就会让该原型对象指向另一个原型对象,从而指向对应的构造函数。如果这个原型对象也是类似的指向,那么就会得到更长的原型链。

下面通过代码来了解下它的实现

image.png

  • 由上面的代码可以看到
  1. SuperType的属性property被赋值为true,SuperType原型中的getSuperValue返回它对应的属性(true),SubType的属性subproperty被赋值为false。
  2. SuperType实例给SubType原型对象赋值,于是SubType继承了SuperType中的getSuperValue方法以及对应的属性property(true),之后SubType原型调用getSuperValue方法得到它对应的属性 (SubType的原型指向了另一个对象——SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。)

image.png 3. instance实例指向SubType的原型,因此得到了属性Subproperty、以及SubType继承的superType的原型中的property以及方法。这里由于原型链的缘故,instance中的构造函数指向superType,因此得到的是property(true)

总结:在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用instance.getSuperValue()会经历三个搜索步骤:1. 搜索实例 2. 搜索SubType.prototype 3. 搜索SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

默认的原型

  • 所有引用类型默认都继承了Object,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。因此上面代码完整的原型图

image.png

  • 因此,当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那个方法。

确定原型和实例的关系

  1. 使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。

image.png

  • 由于原型链的关系,我们可以说instance是Object、SuperType或SubType中任何一个类型的实例。因此,测试这三个构造函数的结果都返回了true。
  1. 使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都会返回true。

image.png

原型链的问题

  1. 在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

image.png

  • 这个例子中的SuperType构造函数定义了一个colors属性,该属性包含一个数组(引用类型值)。SuperType的每个实例都会有各自包含自己数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此它也拥有了一个它自己的colors属性——就跟专门创建了一个SubType.prototype.colors属性一样。

    但结果是什么呢?

  • 结果是SubType的所有实例都会共享这一个colors属性。而我们对instance1.colors的修改能够通过instance2.colors反映出来,就已经充分证实了这一点。

  1. 在创建子类型的实例时,不能向父类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超(父)类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。

组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承

  • 指的是将原型链和借用构造函数的技术组合到一块,结合两者优势的一种继承模式。
  • 原理:使用原型链实现对原型属性和方法的继承,借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

image.png

  1. SuperType构造函数定义了两个属性:name和colors。
  2. SuperType的原型定义了一个方法sayName()。
  3. SubType构造函数在调用SuperType构造函数时传入了name参数(使用call或者apply都可),紧接着又定义了它自己的属性age。
  4. 将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType实例既分别拥有自己属性——包括colors属性,又可以使用相同的方法了。

优势:组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。