醉临武-临武县第一中学官网

探索 React 中 ES6 的继承机制

什么是继承呢?

继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别A“继承自”另一个类别B,就把这个A称为“B的子类别”,而把B称为“A的父类别”也可以称“B是A的超类”。继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。

es 5 中经典的继承

什么是原型链



instance.__proto__ ---> B.prototype
B.prototype.__proto__ ---> A.prototype
以此类推

实例访问方法就是通过 __proto__ 来访问类的实例对象上的方法,其追踪过程也是通过 B.prototype.__proto__ 来向父类追踪

1.对象有属性 __proto__, 指向该对象的构造函数的原型对象。
2.方法除了有属性 __proto__, 还有属性prototype,prototype指向该方法的原型对象。
(类/方法本身的 __proto__ 指向 js 方法本身的定义,无意义)

es5 的继承实现

  • 方案一

function inherits (Parent, Child) {
    Child.prototype = new Parent() // Child.prototype.__proto__ === Parent.prototype
    Child.prototype.constructor = Child 
}
  • 当父类构造函数含参时不可用*

  • 方案二

function inherits (Parent, Child) {  let Temp = function () {}
  Temp.prototype = Parent.prototype
  Child.prototype = new Temp()
  Child.prototype.constructor = Child  
}
  • 方案三

function inherits (Parent, Child) {
  Child.prototype = Object.create(Parent.prototype) // Child.prototype = Parent.prototype, 这里是直接将原型拷贝了。此时 Child 就是拓展后的 Parent
  Child.prototype.constructor = Child  
}

使用

function Student(props) {
  this.name = props.name || 'Unnamed'}function PrimaryStudent(props) {
  Student.call(this, props) // 必须要执行的一段代码,相当于 class 构造函数中的 super(props), 主要用来生成父类实例(继承相当于是对父类实例的进一步加工修改)
  this.grade = props.grade || 1
}

// 实现原型继承链:
inherits(PrimaryStudent, Student)

es 5 中继承的实际应用

此处我们需要 hack 的是 commonmark 的 HtmlRenderer 类中的一段渲染函数。 commonmark 是一个 markdown 渲染库

import commonmark from 'commonmark'const MyRenderer = function (options) { // 构造函数,绑定 this  return commonmark.HtmlRenderer.call(this, options)
}

MyRenderer.prototype = Object.create(commonmark.HtmlRenderer.prototype) // 使用方案三实现继承

MyRenderer.prototype.image = function (node, entering) {
  node.destination = this.options.convertHref(node.destination) // 任何形式的 hack 代码  return commonmark.HtmlRenderer.prototype.image.call(this, node, entering) // 使用 hack 好的新数据、this 对原有方法调用返回结果
}

箭头函数

箭头函数 是 ecma 标准 中的官方定义,主要是为了解决函数编译时函数中 this 的指向问题。而在 Java8 中则对应有 Lambda 表达式,而它主要是解决了单函数接口的写法简洁性问题,在使用 Java7 的 IDE 中,原有的写法也会被处理为箭头函数的显示方式。

箭头函数的相应等价写法

在箭头函数之前,我们都使用匿名函数搭配 bind(this) 的方式来绑定编译时上下文。因此经常会出现一下代码:

(function () {...}).bind(this)

这样就把当前匿名函数绑定为上下文的 this。
(普通函数 or 匿名函数中的 this 指向 window 或者 undefined,而只有在运行时指定调用者时,才会指向调用者)

而箭头函数只需要

() => {...}

因为一些问题的进一步优化

硬代码的每一次执行,都会创建一个新的函数。
如果出现一些需要函数唯一性的场景,滥用箭头函数产生的弊端逐步显露。
典型场景——“订阅与反订阅”

BackAndroid.addEventListener('hardwareBackPress', this.handleBack)
BackAndroid.removeEventListener('hardwareBackPress', this.handleBack)

RN 中对返回事件的监听,由源码可知,在反订阅的执行中是由第一个参数(标记量)和第二个标记量(函数引用)共同决定的,此时如果因为函数的执行而产生了一个新的函数,反订阅会失败的。

此时就有了将该函数在单次执行的生命周期中存入当前类的实例 this 上(比较通用的有 constructor)。

constructor () {
  super()
  this.handleBack = () => {}
  or
  this.handleBack = (function () {}).bind(this)
}

再后来,React 又加入了在 class 中对箭头函数的支持,因此可以直接这么写:

class A {
  arrowFunction = () => {}
}

(注意,这种写法还是相当于声明在了实例 this 上,而非 A 的原型 prototype 上)

class 继承中的箭头函数

之前为了拓展一个类,突发奇想想到了 es6 的 class (虽然知道它就是 es5 原型链继承的语法糖),凭借对 C++ 以及 Java 继承经验的类比,最后成功的,跌入了 es6 的深坑:

  • 第一步,我们使用普通创建类的方式声明了一个类 A

class A {
  files = []

  startUpload = () => {
    ...
  }
}

这是一个文件上传的简易类,此时我们又一个需求,就是在某种情况下需要实现在 startUpload 之前对 files 进行重新排序,为了避免强耦合,本来打算用组合或者注入高阶函数的方式实现,最后还是选择了继承。

class B extends A {  sort () {
       ...
  }
  startUpload = () => {
    this.sort()
    super.startUpload()
  }
}

然而问题出现了,我们使用图中的方式根本无法实现真正意义上的拓展,问题就出在了箭头函数上:

  • 第二步分析:
    箭头函数是被声明在了对应的 this 上,因此在子类上二次声明 startUpload 方法是对父类 this 上属性的覆盖,而非并存。而且在语法层面上 es6 根本不允许在箭头函数上访问关键字 super
    因此解决方案就是将箭头函数全部替换为对应的普通方法,将方法放到类的原型上,这样就可以实现了方法的继承,而非覆盖。

总结

  1. 子类中的非静态方法中,super 指向父类原型。

  2. 静态方法中,super 指向父类。

  3. 由于 super() 方法的执行实际上是 父类.prototype.constructor.call(this, props) 等价于 父类.call(this, props)。 父类从构造开始,this 就被指定为了子类的 this,此时可以视作父子类 this 的合并。此时 super === this,因此赋值的时候是 this,而在取值的时候则遵循 1 。

  4. 在生成实例的时候,会对子类父类 this 上的属性进行合并,如果有重名的,则子类覆盖父类。

  5. 同箭头函数一样,所有直接声明在类体上的属性变量都是在 this 上的,类似于 state、箭头函数的声明。

  6. 使用实例访问对象内容时,优先访问实例 this 上的内容,其次访问子类原型上的内容,在找不到对应内容后,再一层一层以原型链为线索向上查找内容。

  7. 在普通方法中使用 this 访问内容时,和使用子类实例等价。