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