简单了解TypeScript中如何继承 Error 类
前言
在javascript 中很多时候都需要自定义错误,尤其是开发 node.js 应用的时候。 比如一个典型的网站服务器可能需要有 networkerror, databaseerror, unauthorizederror 等。 我们希望这些类都拥有 error 的特性:有错误消息、有调用栈、有方便打印的 tostring 等。 最直观的实现方式便是 继承 error 类。 但考虑 typescript 需要编译到 es5 兼容性问题会较为复杂, 本文用来帮助理解 typescript 中继承 error 的问题来源以及对应的几种解决方式。
我们需要怎样的 customerror
为了容易讨论最佳实践,首先明确我们自定义的 customerror 需要做到哪些功能。 下面是 harttle 的观点:
- 可以调用 new customerror() 来创建,并且 instanceof error 操作应该返回 true。可以用来创建是基本要求,能够被视为 error 的实例能够兼容既有系统(比如 tostring() 要返回调用栈),同时符合惯例。
- .stack 属性首行应为 customeerror: <message>。如果是 error: <message> 可能就没那么漂亮。
- .stack 属性应当包含调用栈并指向 new customerror() 的那一行。这一点可能是关键,如果指向 customerror 构造函数中的某一行,就会给这个类的使用方造成困惑。
下面举个例子,这是一个 message 为 "intended" 的 customerror 的 .stack 属性值:
customerror: intended at object.<anonymous> (/users/harttle/downloads/bar/a.js:10:13) at module._compile (module.js:653:30) at object.module._extensions..js (module.js:664:10) at module.load (module.js:566:32) at trymoduleload (module.js:506:12) at function.module._load (module.js:498:3) at function.module.runmain (module.js:694:10) at startup (bootstrap_node.js:204:16) at bootstrap_node.js:625:3
es5 中如何继承 error?
error 是一个特殊的对象,或者说 javascript 的 new 是一个奇葩的存在。 为方便后续讨论,我们先讨论组 es5 时代是怎样继承 error 的。 我们说 javascript 是一门混杂的语言,如何继承 error 就是一个典型的例子。 如果你熟悉 原型继承的方式,应该会写出如下代码:
function customerror (message) { error.call(this, message) } customerror.prototype = new error()
因为 stack 只在 new 的时候生成,上述实现不能满足功能 2 和功能 3,也就是说:
- stack 的第一行是总是 error 而不是 customerror 且不包含 message 信息。
- stack 总是指向 new error() 的那一行,而不是 new customerror()。
node 文档 中描述了一个 capturestacktrace 方法来解决这个问题,改动后的实现如下:
function customerror (msg) { this.name = 'customerror' this.message = msg error.capturestacktrace(this, customerror) } customerror.prototype = new error()
其中 .capturestacktrace() 会使用传入对象的 name 和 message 来生成 stack 的前缀;同时第二个参数用来指定在调用栈中忽略掉哪一部分,这样栈就会指向 new customerror 的地方而不是 capturestacktrace() 的地方。
es6 中如何继承 error?
既然 es6 通过 class 和 extends 等关键字给出了类继承机制, 那么想必通过编写 customerror 类来继承 error。事实也确实如此,只需要在构造函数中调用父类构造函数并赋值 name 即可实现文章开始提到的三个功能:
class customerror extends error { constructor(msg) { super(msg) this.name = 'customerror' } }
typescript 中如何继承 error?
es6 中提供了 new.target 属性, 使得 error 的构造函数中可以获取 customerror 的信息,以完成原型链的调整。 因此 typescript 需要编译到 es5 时上述功能仍然是无法自动实现。 在 typescript 中的体现是形如上述 es6 的代码片段会被编译成:
var customerror = /** @class */ (function (_super) { __extends(customerror, _super); function customerror(msg) { var _this = _super.call(this, msg) || this; _this.name = 'customerror'; return _this; } return customerror; }(error));
注意 var _this = _super.call(this, msg) || this; 中 this 被替换掉了。 在 typescript 2.1 的 changelog 中描述了这个 breaking change。 **这会造成 customerror 的所有对象方法都无法使用,这里介绍几种 workaround:
题外话,这个分支可能会导致测试覆盖率中的 分支未覆盖问题。可以只在 es6 下产生测试覆盖报告来解决。
1. 使用 setprototypeof 还原原型链
这是 typescript 官方给出的解决方法,见这里。
class customerror extends error { constructor(message) { super(message); object.setprototypeof(this, fooerror.prototype); } }
注意这是一个性能很差的方法,且在 es6 中提出,兼容性也很差。在不兼容的环境下可以使用 __proto__ 来替代。
2. 坚持使用 es5 的方式
不使用 es6 特性,仍然使用本文前面介绍的 『es5 中如何继承 error?』给出的方法。
3. 限制对象方法的使用
虽然 customerror 的对象函数无法使用,但 customerror 仍然支持 protected 级别的方法供子类使用,阉割的地方在于自己不能调用。 由于 javascript 中对象属性必须在构造函数内赋值,因此对象属性也不会受到影响。也就是说:
class customerror extends error { count: number = 0 constructor(msg) { super(msg) this.count // ok,属性不受影响 this.print() // typeerror: _this.print is not a function,因为 this 被替换了 } print() { console.log(this.stack) } } class derivederror extends customerror { constructor(msg) { super(msg) super.print() // ok,因为 print 是直接从父类原型获取的,即 `_super.prototype.print` } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。