继承

1. 原型链式继承

1.1 原型模式

原型模式是JavaScript中创建对象的一种最常见的方式。JavaScript是一种弱类型的语言,没有类的概念,也不是一种面向对象的语言。但是,在JavaScript中,借助函数的原型(也就是prototype)可以实现类的功能。

使用原型模式创建对象的基本做法如下:

  1. function Person (name) {
  2. this.name = name // 私有属性
  3. }
  4. // 公共方法
  5. Person.prototype.sayName = function () {
  6. console.log(this.name)
  7. }
  8. // 创建实例
  9. var personA = new Person('A')
  10. console.log(personA.name) // A
  11. personA.sayName() // A
  12. var personB = new Person('B')
  13. console.log(personB.name) // B
  14. personB.sayName() // B

在以上代码中,两个实例既拥有各自不同的属性 name, 又共享了 公有的方法 sayName(),这样就实现了类似于强类型语言中类的概念。

这种功能的实现,就得益于构造函数的prototype属性。下图展示了构造函数、原型、实例三者之间的关系。

原型模式.png

每个构造函数都有一个属性 prototype,prototype属性指向一个对象,该对象被构造函数的所有实例所共享,我们称这个对象为构造函数的原型

原型与构造函数之间通过prototype属性和constructor属性相互联系。

实例与原型之间通过一个 _proto_属性(是浏览器中存在的一个虚拟的属性,各个浏览器的实现不同,谷歌中为_proto_)产生联系。

一个构造函数创建的不同的实例都可以通过 _proto_属性 访问到它的原型,这就是为什么上面的示例中两个示例可以访问到共同的 sayName() 方法的原因。

1.2 原型链式继承

1.2.1 原型链与原型链式继承

构造函数的原型并不是固定不变的,我们可以人为的切断构造函数与其原型之间的联系,也可以给构造函数指定自定义的原型。

  1. function Person (name) {
  2. this.name = name
  3. }
  4. Person.prototype = {
  5. constructor: Person,
  6. sayName: function () {
  7. console.log(this.name + '-hello')
  8. }
  9. }
  10. var personA = new Person('A')
  11. personA.sayName() // A-hello

上面的代码我们自己给Person构造函数指定了一个原型,并添加了一个sayName()的公共方法。

原型链的实现就是基于原型可以重写这一基本前提。

我们结合下图来说明什么是原型链,它与JavaScript中实现类之间的继承的关系。

JavaScript-原型链.png

上图中,绿色线条所形成的链条即被我们称之为原型链。基于原型链,我们可以实现一种继承方式。

以下是一段基于原型链实现继承代码示例,为了区别于Java等语言中的class关键字,这里以Type表示类型,同时以 Super表示父,以Sub表示子,将SuperType称之为超类型,将SubType称之为子类型。

  1. function SuperType () {
  2. this.superName = 'superName'
  3. }
  4. SuperType.prototype.sayName = function () {
  5. console.log(this.name)
  6. }
  7. function SubType (subName) {
  8. this.name = subName || 'subName'
  9. }
  10. SubType.prototype = new SuperType()
  11. var subA = new SubType('A')
  12. subA.sayName() // A
  13. console.log(subA.superName) // superName

以上代码中,我们并没有为SubType定义sayName() 方法,SubType中也没有superName属性,但SubType创建的示例subA却可以使用sayName()方法打印出 自己的name属性,同时可以访问到并非自身定义的 superName属性,这是因为我们将SuperType的一个实例指定为了SubType的原型,因此subA与new SuperType()与 SuperType.prototype之间就有了一条原型链,subA因此可以访问到链条能触及的所有属性和方法。

这就是原型链实现继承的一个基本方式。

1.2.2 原型链式继承的缺陷

尽管,在理解了原型链后,原型链式的继承方式变得很好理解,使用也很简单。但是,原型链式的继承方式却并非完美的实现继承的解决方案。原型链式的继承也存在其自身的问题。让我们看一段代码:

  1. function SuperType () {
  2. this.element = ['A', 'B', 'C']
  3. }
  4. SuperType.prototype.printElement = function () {
  5. console.log(this.element)
  6. }
  7. function SubType () {
  8. }
  9. SubType.prototype = new SuperType()
  10. var subA = new SubType()
  11. subA.element.push('D')
  12. subA.printElement() // ["A", "B", "C", "D"]
  13. var subB = new SubType()
  14. subB.element.push('E')
  15. subB.printElement() // ["A,", "B", "C", "D","E"]

让我们感到奇怪的是,两个不同的实例分别向element属性中各自添加不同元素后,subB中打印的element属性中竟然包含了subA添加的元素。

这也很好理解,因为两个实例都继承自SuperType的同一个实例构成的原型,所以他们共享了超类型构造函数中的element属性,因此,理所应当的,前一个实例对element元素所作的更改会体现在后一个实例上。

但是,这并不是我们想要的结果。

我们知道,使用原型模式创建对象时,会把私有属性(方法)和共有属性(方法)分开定义,私有属性定义在构造函数中,公有属性定义在原型中。因此,显然,当我们使用原型链实现继承时,我们不仅继承了超类型实例的原型中的公有属性,也继承了其构造函数中定义的私有属性并且本应该是私有的属性,却因为它成了子类型的原型,而变成了子类型的公有属性。

这是一个令人头疼的问题。

原型链式继承的另一个问题是,子类型的构造函数中,无法给超类型传递参数。这也局限了这种方式在实际开发中的应用。
缺点:

  • 当使用原型链实现继承时,子类型不仅继承了超类型实例的原型中的公有属性,也继承了其构造函数中定义的私有属性。并且本应该是私有的属性,却因为它成了子类型的原型,而变成了子类型的公有属性;
  • 原型链式继承的另一个问题是,子类型的构造函数中,无法给超类型传递参数

2. 借用构造函数

2.1 借用构造函数实现继承

为了解决原型链式继承所带来的问题,开发人员使用了一种新的技术,这种技术被称为借用构造函数的技术。其具体实现方法如下:

  1. function SuperType (name) {
  2. this.name = name || 'superName'
  3. this.element = ['A', 'B', 'C']
  4. }
  5. function SubType (name) {
  6. SuperType.apply(this, arguments) // SuperType.call(this, name)
  7. }
  8. var subA = new SubType('subA')
  9. subA.element.push('D')
  10. console.log(subA.element) // ["A", "B", "C", "D"]
  11. console.log(subA.name) // subA
  12. var subB = new SubType('subB')
  13. subB.element.push('E')
  14. console.log(subB.element) // ["A", "B", "C", "E"]
  15. console.log(subB.name) // subB

根据程序的执行现象,我们可以看到,SubType的两个实例,分别向element属性中添加了不同的元素,从打印结果发现,subA中添加的元素并没有反映到subB中,SubType的两个实例在继承了SuperType中的自有属性的同时,又各自保留了其属性的副本。

另外,在SubType构造函数中,调用SuperType构造函数时,子类型可以给超类型传递参数。

借用构造函数看似简单,却解决了原型链式继承中存在的两个让人头疼的问题。

实际上,借用构造函数的思想与我们平时编码时常用的技巧 函数的提取 类似,下面我们通过一段代码来简要的讲解一下这个过程(以下代码部分为伪代码,在此不探讨其合理性):

  1. function SubType () {
  2. this.propertyA = 'propertyA'
  3. this.propertyB = 'propertyB'
  4. this.propertyC = 'propertyC'
  5. this.propertyD = 'propertyD'
  6. this.propertyE = 'propertyE'
  7. }

以上是一个子类型的构造函数,其中有诸多属性,在许多其他的子类型中,这些属性也被需要。

这跟函数提取的场景很类似,在一个函数中,某些代码具备一定的复用性,此时,我们很容易的想到,把这些代码提取到一个单独的函数中,此后只要有需要的函数,都可以复用这些代码。这一步可以简单实现为如下:

  1. function SubType () {
  2. SuperType()
  3. }
  4. function SuperType () {
  5. this.propertyA = 'propertyA'
  6. this.propertyB = 'propertyB'
  7. this.propertyC = 'propertyC'
  8. this.propertyD = 'propertyD'
  9. this.propertyE = 'propertyE'
  10. }

SubType构造函数内部调用SuperType构造函数,将SuperType中的属性复用到SubType中。

但是,如只是简单的提取,则会产生一个问题,SuperType中的this在SuperType被定义时,即已确定。SuperType被定义在全局作用域下,因此this指向全局作用域(一般是window)。简单的提取调用后,则SubType中实际上会变成这样:

  1. function SubType () {
  2. window.propertyA = 'propertyA'
  3. window.propertyB = 'propertyB'
  4. window.propertyC = 'propertyC'
  5. window.propertyD = 'propertyD'
  6. window.propertyE = 'propertyE'
  7. }

当创建SubType的新实例时,通过调用相应的属性,会得到如下结果:

  1. function SubType () {
  2. SuperType()
  3. }
  4. function SuperType () {
  5. this.propertyA = 'propertyA'
  6. this.propertyB = 'propertyB'
  7. this.propertyC = 'propertyC'
  8. this.propertyD = 'propertyD'
  9. this.propertyE = 'propertyE'
  10. }
  11. var subA = new SubType()
  12. console.log(subA.propertyA) // undefined
  13. console.log(window.propertyA) // propertyA

这与以上的描述一致。

为了让SuperType在调用时,其包含的属性能够正确的复用到SubType中,只需要将SuperType的执行环境绑定到SubType上即可。使用apply()方法和call()方法都可以很容易实现这一步。上述实现就变为:

  1. function SubType () {
  2. SuperType.apply(this,arguments)
  3. }
  4. function SuperType () {
  5. this.propertyA = 'propertyA'
  6. this.propertyB = 'propertyB'
  7. this.propertyC = 'propertyC'
  8. this.propertyD = 'propertyD'
  9. this.propertyE = 'propertyE'
  10. }
  11. var subA = new SubType()
  12. console.log(subA.propertyA) // propertyA
  13. console.log(window.propertyA) // propertyA

以上的过程或许不够准确,但的确可以帮助理解借用构造函数的实现思路。

由以上的代码示例,我们可以看到,借用构造函数的的确确是解决了原型链链式继承方法的缺陷

  • 每个实例都可以保持超类型中自有属性的私有性,每个子类实例中都可以保有超类型中自有属性的一个副本,子类实例之间对继承而来的自有属性的操作不会相互干扰;
  • 子类型的构造函数可以向超类型的构造函数中传递参数

2.2 借用构造函数的缺陷

但是,借用构造函数中也存在着令人头疼的问题。

细心的读者会发现,在以上的关于借用构造函数讲解示例中,竟没有出现一次公共方法的调用。没错,这正是问题所在。

简单来说就是,我(借用构造函数)做不到啊。

也许你会进行一些尝试,我给超类型的原型定义公共方法行不行呢?我直接在超类型构造函数上定义一个方法行不行呢?一起来看下面这两段代码:

示例一:给超类型的原型定义公共方法

  1. function SuperType (name) {
  2. this.name = name || 'superName'
  3. this.element = ['A', 'B', 'C']
  4. }
  5. SuperType.prototype.sayName = function () {
  6. console.log(this.name)
  7. }
  8. function SubType (name) {
  9. SuperType.apply(this, arguments) // SuperType.call(this, name)
  10. }
  11. var subA = new SubType('subA')
  12. subA.sayName() // Uncaught TypeError: subA.sayName is not a function

毫不吃惊,程序执行出错, subA.sayName不是一个function;

借用构造函数的本质仅仅是将超类型中的属性复制一份到子类型中,并将其属性的执行环境绑定到子类型上,因此子类型在执行完超类型构造函数那一刻,子类型和超类型之间就切断了联系,子类型的实例又怎么可能访问到超类型的原型方法呢。

实例二:直接在超类型构造函数上定义方法

  1. function SuperType (name) {
  2. this.name = name || 'superName'
  3. this.element = ['A', 'B', 'C']
  4. this.sayName = function () {
  5. console.log(this.name)
  6. }
  7. }
  8. function SubType (name) {
  9. SuperType.apply(this, arguments) // SuperType.call(this, name)
  10. }
  11. var subA = new SubType('subA')
  12. subA.sayName() // 'subA'
  13. var subB = new SubType('subB')
  14. subB.sayName() // 'subB'

看似可行,实则???

  1. console.log(subA.sayName === subB.sayName) // false

呃。。。。。

虽然都实现了相同的功能,但两个方法并不是同一个方法。还是上面说的,借用构造函数仅仅只是复制了一份超类型中的属性和方法,这并不是复用,借用构造函数无法实现公共方法的复用。

基于借用构造函数的以下两个缺陷:

  • 无法定义子类型可复用的公共方法;
  • 无法访问超类型的原型;

借用构造函数在实际应用中很少单独使用。

3. 组合继承

虽然,原型链模式和借用构造函数模式都无法完美实现继承,但所幸二者的缺陷可以互补。自然而然的,一种相对完美的解决方案出现了,即组合继承。

将原型链式继承和借用构造函数继承组合起来,使用原型链模式实现对超类型的公共属性和公共方法的继承,使用借用构造函数模式实现对超类型中自有属性的继承。这样,既通过在原型上定义方法实现了函数的复用,又能够保证每个子类实例都能保有一份超类型中的自有属性。

组合继承的实现方法如下:

  1. function SuperType (name) {
  2. this.name = name || 'superName'
  3. this.element = ['A', 'B', 'C']
  4. }
  5. SuperType.prototype.sayName = function () {
  6. console.log(this.name)
  7. }
  8. SuperType.prototype.printElement = function () {
  9. console.log(this.element)
  10. }
  11. function SubType (name) {
  12. SuperType.apply(this, arguments)
  13. }
  14. SubType.prototype = new SuperType ()
  15. var subA = new SubType('subA')
  16. subA.element.push('subA')
  17. subA.sayName() // subA
  18. subA.printElement() // ["A", "B", "C", "subA"]
  19. var subB = new SubType('subB')
  20. subB.element.push('subB')
  21. subB.sayName() // subB
  22. subB.printElement() // ["A", "B", "C", "subB"]
  23. console.log(subA.sayName === subB.sayName) // true

组合继承,也称之为 伪经典继承,指的是将原型链和借用构造函数的技术组合在一起,从而发挥二者之长的一种继承模式

其思路是,使用原型链实现对原型方法的继承借用构造函数实现对实例属性的继承

  1. function SuperType () {
  2. this.element = ['AA', 'BB', 'CC']
  3. }
  4. SuperType.prototype.printElement = function () {
  5. console.log(this.element)
  6. }
  7. function SubType () {
  8. SuperType.apply(this, arguments)
  9. }
  10. SubType.prototype = new SuperType()
  11. SubType.prototype.constructor = SubType
  12. var subA = new SubType()
  13. subA.element.push('DD')
  14. subA.printElement() // ["AA", "BB", "CC", "DD"]
  15. var subB = new SubType()
  16. subB.element.push('EE')
  17. subB.printElement() // ["AA", "BB", "CC", "EE"]
  18. console.log(subA.printElement === subB.printElement) // true

组合继承解决了原型链模式和借用构造函数模式的缺陷,融合了它们的优点,是JavaScript中最常用的继承模式。

但组合模式也有自己的缺陷。下文中第5种继承实现方法将解决组合模式的缺陷,在此之前,我们先看第四种继承的实现方法。

4. 原型式继承和寄生式继承

4.1 原型式继承

原型式继承的基本实现方式如下

  1. function getSubType (SuperType) {
  2. function F () {}
  3. F.prototype = SuperType
  4. return new F()
  5. }

这种方法没有严格意义上的构造函数,其思想是借助原型,可以基于已有的对象创建新对象,同时还不必为此创建自定义的类型。

在getSubType函数内部,先创建一个临时性的构造函数,然后将传入的对象 SuperType 作为这个构造函数的原型,最后返回一个这个临时性构造函数的新实例。

这种原型式继承,要求必须有一个对象作为另一个对象的基础。将该对象传入getSubType函数,再根据具体需求对得到的对象加以修改即可。

  1. function getSubType (SuperType) {
  2. function F () {}
  3. F.prototype = SuperType
  4. return new F()
  5. }
  6. var superObject = {
  7. name: 'SUPER',
  8. element: ['AA', 'BB', 'CC', 'DD'],
  9. printElement: function () {
  10. console.log(this.element)
  11. }
  12. }
  13. var subA = getSubType(superObject)
  14. subA.name = 'SUBA'
  15. subA.element.push('EE')
  16. subA.printElement() // ["AA", "BB", "CC", "DD", "EE"]
  17. console.log(subA.name) // SUBA
  18. var subB = getSubType(superObject)
  19. subB.printElement() // ["AA", "BB", "CC", "DD", "EE"]
  20. console.log(subB.name) // SUPER

ECMAScript5中的Object.create()方法规范化了原型式继承,这个方法接受两个参数,一个是用作新对象原型的对象,另一个是为新对象定义额外属性的对象。

4.2 Object.create() 方法

Object.create()方法接受两个参数:

  • 一个是用作新对象原型的对象(被继承者);
  • 另一个是为新对象定义额外属性的对象;

第二个参数的每个属性都是自定义的,以这种方式定义的任何属性都会覆盖原型对象上的同名属性

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

  1. var person = {
  2. isHuman: false,
  3. printIntroduction: function() {
  4. console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
  5. }
  6. };
  7. var me = Object.create(person);
  8. me.name = 'Matthew'; // "name" is a property set on "me", but not on "person"
  9. me.isHuman = true; // inherited properties can be overwritten
  10. me.printIntroduction();
  11. // expected output: "My name is Matthew. Am I human? true"

其使用语法如下:

  1. Object.create(proto,[propertiesObject])
  • proto:新创建对象的原型对象。
  • propertiesObject:可选。需要传入一个对象。如果该参数被指定且不为undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。

4.3 寄生式继承

寄生式继承的实现思路与原型式继承紧密相关。

寄生式继承的思路与寄生构造函数工厂模式类似,即:

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回这个对象。

其实现过程可总结为3步:

  • 传入一个用于被继承的基础对象;
  • 生成一个新对象,新对象继承基础对象;
  • 对新对象进行加工,定义自己的属性和方法,返回新对象。

以下是寄生式继承的一个示例:

  1. function createAnother (original) {
  2. // 通过原型式继承创建一个新对象,新对象将原始对象作为自己的原型
  3. function F () {}
  4. F.prototype = original
  5. var clone = new F()
  6. // 通过某种方式来增强这个新对象,添加对象属性
  7. clone.sayHello = function () {
  8. console.log('hello!')
  9. }
  10. // 返回这个新对象
  11. return clone;
  12. }

示例中,createAnother() 函数 将入参(original)作为新对象的基础,通过原型式继承,将original 作为新对象的原型,然后将新创建的以 original 为原型的对象,添加某些新的属性和方法,得到新的对象clone, 这样,clone 对象既继承了 original 对象的属性和方法,又可以自定义自己的属性和方法。

在主要考虑对象,而非自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。

寄生式继承是一种轻量级的继承实现方式。

缺点:函数不能复用,适合用来处理简单的继承。

5. 寄生组合式继承

5.1 组合式继承的缺陷

组合式继承的缺陷在于,无论什么情况下,都会调用2次超类型构造函数。

  • 第一次:创建子类原型时;
  • 第二次:子类型构造函数内部,超类型通过借用构造函数继承其实例属性时;
  1. function SuperType (element) {
  2. this.element = element||['AA', 'BB', 'CC']
  3. }
  4. SuperType.prototype.printElement = function () {
  5. console.log(this.element)
  6. }
  7. function SubType (element) {
  8. SuperType.apply(this, arguments) // 第二次调用SuperType()
  9. }
  10. SubType.prototype = new SuperType() // 第一次调用 SuperType()
  11. SubType.prototype.constructor = SubType

第一次调用SuperType时,new SuperType()会创建 element 属性,即SubType的原型上拥有属性element,

第二次调用SuperType时,实际上是 将SuperType上的属性复制到了SubType上,即SubType拥有实例属性 element,

我们知道,实例属性会覆盖原型属性,因此,第一次调用SuperType生成的属性会被第二次的属性覆盖掉,并且无论如何都会被覆盖掉。

那么,既然第一次创建原型属性的操作无论如何都会被覆盖掉,那这个操作就是在做无用功了,那么该如何解决这个问题呢?

答案就是: 寄生组合式继承

5.2 寄生组合式继承

实现方法

  • 通过借用构造函数来继承属性;
  • 通过原型链的混成形式来继承方法;

基本思路

不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的,无非就是超类型的原型的一个副本而已。

本质上,就是使用寄生式继承来继承超类型的原型,然后再将这个原型赋值给子类型的原型。

基本模式

  1. function inheritPrototype(SubType, SuperType) {
  2. function F () {}
  3. F.prototype = SuperType.prototype
  4. var prototype = new F() // 创建一个对象,该对象拥有SuperType的原型方法和属性,却不包含SuperType的实例属性
  5. prototype.constructor = SubType // 重新指定constructor属性
  6. // 将基于SuperType创建的新类型的实例作为 SubType 的原型
  7. SubType.prototype = prototype
  8. }

以上就是通过寄生式继承来实现继承超类型原型方法的模式。

完整示例

  1. function SuperType (name) {
  2. this.name = name
  3. }
  4. SuperType.prototype.sayName = function () {
  5. console.log(this.name)
  6. }
  7. function SubType (name, age) {
  8. SuperType.call(this, name) // 继承超类型的实例属性
  9. this.age = age
  10. }
  11. inheritPrototype(SubType, SuperType) // 继承超类型的原型
  12. SubType.prototype.sayAge = function () {
  13. console.log(this.age)
  14. }

6. es6中的继承

6.1 简介

es6中引入了 class 的概念,同时也支持使用新的方式,实现继承,也就是 extends 方式。
class 通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
示例

  1. class Point {
  2. constructor (x, y) {
  3. this.x = x
  4. this.y = y
  5. }
  6. toString () {
  7. }
  8. }
  9. class ColorPoint extends Point {
  10. constructor (x, y, color) {
  11. super (x, y) // 调用父类的constructor(x, y)
  12. this.color = color
  13. }
  14. toString () {
  15. return this.color + super.toString() // 调用父类的toString()
  16. }
  17. }

如果子类没有定义 constructor 方法,这个方法会被默认添加,也就是说,不管有没有显示定义,任何一个子类都有 constructor 方法。

  1. class ColorPoint extends Point {
  2. }
  3. // 等同于
  4. class ColorPoint extends Point {
  5. constructor (...args) {
  6. super(...args)
  7. }
  8. }

在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。
image-20210615170148411.png
这是因为,子类实例的构建(子类的this)是基于对父类实例(父类this)的加工,只有 super 方法才能返回父类的实例。
示例:

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x
  4. this.y = y
  5. }
  6. }
  7. class ColorPoint extends Point {
  8. constructor(x, y, color) {
  9. this.color = color; // ReferenceError
  10. super(x, y);
  11. this.color = color; // 正确
  12. }
  13. }

6.2 super 关键字

super表示父类的构造函数,用来新建父类的 this 对象。
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错 。
这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。

  1. class Point { /* ... */ }
  2. class ColorPoint extends Point {
  3. constructor() {
  4. }
  5. }
  6. let cp = new ColorPoint(); // ReferenceError

image-20210615161024942.png

super 的两种用法:

  • 当作函数使用 ;
  • 当作对象使用 ;

1. 当做函数使用

super 作为函数调用时,代表父类的构造函数。

ES6 要求,子类的构造函数必须执行一次 super 函数。

作为函数时,super() 只能用在子类的构造函数中,用在其他的地方就会报错。

image-20210615172659131.png

  1. 当做对象使用

    super作为对象时,在普通方法中,指向父类的原型对象,在静态方法中,指向父类

示例:

  1. class A {
  2. p() {
  3. return 2
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super()
  9. console.log(super.p()) // 2
  10. }
  11. }
  12. let b = new B()

上面代码中,子类 B 当中的 super.p() ,就是将 super当作一个对象使用。
这时, super 在普通方法之中,指向 A.prototype ,所以`super.p()就相当于 A.prototype.p()。

6.3 源码分析

下面看一断es6中实现继承的简单示例代码:

es6:

  1. class SubType extends SuperType {
  2. constructor (name, age) {
  3. super(name)
  4. }
  5. }
  6. class SuperType {
  7. constructor(name) {
  8. this.name = name
  9. }
  10. sayName() {
  11. console.log(this.name)
  12. }
  13. }

对应 es5:

  1. "use strict";
  2. // 创建class ,添加原型属性和静态属性
  3. var _createClass = function() {
  4. function defineProperties(target, props) {
  5. for (var i = 0; i < props.length; i++) {
  6. var descriptor = props[i];
  7. descriptor.enumerable = descriptor.enumerable || false;
  8. descriptor.configurable = true;
  9. if ("value" in descriptor) descriptor.writable = true;
  10. Object.defineProperty(target, descriptor.key, descriptor);
  11. }
  12. }
  13. return function(Constructor, protoProps, staticProps) {
  14. if (protoProps) defineProperties(Constructor.prototype, protoProps);
  15. if (staticProps) defineProperties(Constructor, staticProps);
  16. return Constructor;
  17. };
  18. } ();
  19. function _classCallCheck(instance, Constructor) {
  20. // 调用检测,类不能当成 function 来调用
  21. if (! (instance instanceof Constructor)) {
  22. throw new TypeError("Cannot call a class as a function");
  23. }
  24. }
  25. // 继承父类实例属性
  26. function _possibleConstructorReturn(self, call) {
  27. if (!self) {
  28. throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
  29. }
  30. return call && (typeof call === "object" || typeof call === "function") ? call: self;
  31. }
  32. // 继承父类原型属性和方法
  33. function _inherits(subClass, superClass) {
  34. // 参数检测
  35. if (typeof superClass !== "function" && superClass !== null) {
  36. throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
  37. }
  38. // 当存在父类且父类有原型对象时,以父类的原型为基础,添加自定义constructor属性,然后赋值给子类的原型
  39. // 即:继承父类的原型属性
  40. // object_1 && object_2 只有都为 true时,返回 object_2, 否则返回 undefined
  41. // object_1 && object_2 返回最前面的为 true 的值,都不为true,返回undefined
  42. subClass.prototype = Object.create(superClass && superClass.prototype, {
  43. constructor: {
  44. value: subClass,
  45. enumerable: false,
  46. writable: true,
  47. configurable: true
  48. }
  49. });
  50. //
  51. if (superClass) {
  52. Object.setPrototypeOf
  53. ? Object.setPrototypeOf(subClass, superClass)
  54. : subClass.__proto__ = superClass;
  55. }
  56. }
  57. var SubType = function (_SuperType) {
  58. // 继承父类
  59. _inherits(SubType, _SuperType);
  60. function SubType(name, age) {
  61. // 调用检测,类不能当成 function来调用(不能没有 new)
  62. _classCallCheck(this, SubType);
  63. // 返回SubType 实例
  64. var _this = _possibleConstructorReturn(this, (SubType.__proto__ || Object.getPrototypeOf(SubType)).call(this, name));
  65. _this.age = age;
  66. return _this;
  67. // call 继承父类实例属性
  68. // || 前后的两种写法,实际上是为了兼容性考虑,es5应写为Super.__prototype__,es6则应写为后一种写法
  69. // 上面的同理
  70. /* const superT = null
  71. if (SubType.__proto__ || Object.getPrototypeOf(SubType)) {
  72. superT = SubType.__proto__ || Object.getPrototypeOf(SubType)
  73. } else {
  74. superT = this
  75. }
  76. superT.call(this, name)
  77. */
  78. }
  79. return SubType;
  80. }(SuperType);
  81. var SuperType = function () {
  82. function SuperType(name) {
  83. _classCallCheck(this, SuperType);
  84. this.name = name;
  85. }
  86. _createClass(SuperType, [{
  87. key: "sayName",
  88. value: function sayName() {
  89. console.log(this.name);
  90. }
  91. }]);
  92. return SuperType;
  93. }();

7. Mixin

Mixin指的是,多个对象合成一个新的对象,新对象具有各个组成成员的接口。

它的最简单的实现如下:

  1. const a = {
  2. a: 'a'
  3. }
  4. const b = {
  5. b: 'b'
  6. }
  7. const c = {...a, ...b} // {a: 'a', b: 'b'}

以上示例中,c可看成是 a和b的mixin

下面给出一个更完备的Mixin的实现,将多个类的接口‘混入’ 另一个类:

  1. function mix (...mixins) {
  2. class Mix {}
  3. for (const mixin of mixins) {
  4. copyProperties(Mix, mixin)
  5. copyProperties(Mix.prototype, mixin.prototype)
  6. }
  7. return Mix
  8. }
  9. function copyProperties (target, source) {
  10. for (const key of Reflect.ownKeys(source)) {
  11. if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
  12. //getOwnPropertyDescriptor()方法返回指定对象上一个自有对应的属性。
  13. // 自有属性指的是直接赋予对象的属性,不需要属性从链上找到的)
  14. let desc = Object.getOwnPropertyDescriptor (source, key)
  15. Object.defineProperty(target, key, desc)
  16. }
  17. }
  18. }