事情起因是在写 Nodejs 服务时,想要统一捕获、处理业务异常错误,于是用到了自定义 Http Error,这个自定义的错误是通过继承 Error 对象来实现的。
但是在中间件中进行错误捕获时,instanceof 却无法得到预期的结果,百思不得其解。仔细想了下,因为使用了 ES6 的 extends 语法,唯一的可能性就是 typescript 编译时,出现了问题。
1. 问题复现
有如下代码:
class MyError extends Error {}
const err = new MyError();
console.log(err instanceof MyError);
使用 tsc 编译为 es5 后执行,预期得到结果应该是 true,但实际上,你只会得到 false。
# npx tsc extends.ts
# node extends.js
false
这是为什么呢?我们接着分析。
2. 问题分析
首先我们打开编译后的 js 文件看下。
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
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));
var err = new MyError();
console.log(err instanceof MyError);
可以看到,ts 编译的 target 肯定不是 es6,所有会多了这么多的帮助函数。不过我们不关心这些,我们关心的只是为什么 instanceof 失效了。
这里就回到了 js 的基础知识,调用 new 关键字后,返回的对象应该是构造函数的实例或者返回的对象,如果构造函数里的返回值是 undefined,那么返回构造函数的实例,所以 instanceof 失效的话,有问题的地方只可能是这段代码:
function MyError() {
return _super !== null && _super.apply(this, arguments) || this;
}
这里的 _super 指的是 Error 构造函数,而这里的逻辑优先返回的是 Error 构造函数返回的结果,而不是 this。
一般来说,直接调用构造函数的返回值都是 undefined,但是不幸的是,Error 返回的却是 error 实例,也就导致了 MyError 的实例其实是 Error 的实例。这点可以通过代码验证。
class MyError extends Error {}
const err = new MyError();
console.log(err instanceof Error);
// true
关于 typescript 为什么这么做,官方也有对应的解释:breaking changes
3. 解决方法
typescript 官方其实也给出了相应的方案,就是重新设置 this 的原型对象。
class MyError extends Error {
constructor() {
super()
Object.setPrototypeOf(this, MyError.prototype);
}
}
const err = new MyError();
console.log(err instanceof MyError); // true
4. 另一种方案
在网上搜索时,也发现了网友的另一种方案。因为前面说了,直接调用 Error 构造函数也会返回一个 error 实例,那我们可不可以改变这种行为?答案当然是可以的,本质上其实就是实现一个 Error 对象。因为个人觉得这样做跟 ts 帮你实现并没有什么本质上区别,这里就贴一下原理代码,不做过多深入。
class MyError implements Error {
message: string
name: string
constructor(...args: any[]) {
Error.apply(this, args)
}
}
MyError.prototype = Error.prototype
const err = new MyError();
console.log(err instanceof MyError); // true
console.log(err instanceof Error); // true