在 Typescript 中继承 Error 对象
通过 Typescript class 继承 Error 实现自定义错误类型并编译到 ES5 时,遇到了一个坑。
class MyError extends Error {}
compilerOptions.target 设为 "es5"。
但是运行起来:
const err = new MyError()
err instanceof Error // true
err instanceof MyError // 结果竟然是 false
原因
使用 Babel/Typescript 编译出的代码有类似的问题
Typescript 2.7.2 编译出的代码
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var MyError = /** @class */ (function (_super) {
__extends(MyError, _super);
function MyError() {
// 问题关键在这里
return _super !== null && _super.apply(this, arguments) || this;
}
return MyError;
}(Error));
原因在于,Error 是一个特殊的存在,即是一个构造函数,也是一个普通函数。以下两种调用皆可返回 error object。
Error('message')
new Error('message')
那么在调用以下函数时,_super 为 Error,返回的即是 Error(this, arguments)
,而不是 this
。
_super !== null && _super.apply(this, arguments) || this;
在 Typescript 中
翻了翻文档,Typescript 2.1 的一些 breaking change 导致对于一些原生对象 (Error/Array/Map) 的继承无法正常工作,应该就是由 generated code 的改变造成的。官方给出的一个建议是:
class FooError extends Error {
constructor(m: string) {
super(m)
// 在 super 之后立刻调用,改变实例的 prototype.
Object.setPrototypeOf(this, FooError.prototype)
}
}
但是这个写法其实相当的傻,因为对于每一个子类的构建函数来说,在改变原型之前,是无法拿到正确的子类实例'this.constructor' 的,所以 Object.setPrototypeOf
需要出现在所有子类的构建函数中。
解决方案
只好把原型继承拿回来了,最终在 target 为 es5 及以下的解决方案:
export class ExtensibleError implements Error {
message: string
name: string
constructor(message?: string) {
Error.apply(this, arguments)
this.message = message || ''
this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor)
}
}
}
ExtensibleError.prototype = Object.create(Error.prototype)
构建一个中间的辅助类,并不直接采用 class 继承 Error,而只实现 Error 接口,采用原型继承,此类的示例可经过 instanceOf
的检验。通过 Error.captureStackTrace
在初始化此类实例时能够捕获调用栈。
class MyError extends ExtensibleError {
}
const err = new MyError()
err instanceof ExtensibleError // true
err instanceof MyError // true
err instanceof Error // true
如果编译目标为 ES6 以上呢?
此时编译器就不需要去帮你转化 class 的实现了,会把你的代码原样输出:
** 但是!** 之前的解决方案在 nodejs 中运行会报错:
ExtensibleError.prototype = Object.create(Error.prototype);
^
TypeError: Cannot assign to read only property 'prototype' of function 'class ExtensibleError...
因为使用 class
关键字声明的 ExtensibleError
是一个叫做类构造器(class constructor)的特殊函数,它的 prototype 是只读的,试图去改变它的话,只有报错(nodejs)和不生效两种可能。
以下的代码在现在的 chrome(V8) 和 firefox(SpiderMonkey) 引擎中执行结果都是一样。
class ExtensibleError {}
ExtensibleError.prototype = Object.create(Error.prototype);
const e = new ExtensibleError;
e instanceof Error // false
如果 target 是 ES6 以上的,简简单单写 class ExtensibleError extends Error {}
就行了。
同时我们的解决方案为了在不同编译 target 下都能正常工作,可以加入一个运行时的检测。
/** 检测当前 runtime 是否支持 es6 class */
let isClassSupported = false
try {
isClassSupported = ((class Test {}).toString().indexOf('class') > -1)
} catch (error) {
}
if (!isClassSupported) {
ExtensibleError.prototype = Object.create(Error.prototype)
}
参考