JavaScript Error继承踩坑记

Error ES6 Class继承

在Web App中,我们通常会创建自定义错误类来区分错误类型。如果使用ES6的Class语法,那么应该有类似如下写法:

class MyError extends Error {  
    constructor (msg) {
        super(msg)
        this.message = msg
        this.name = "MyError"
    }
}

现在我们需要报一个自定义错误,那么有:

var err = new MyError("this is a error message")  

同时,在流程处理中,可能需要通过错误类型执行不同的处理逻辑代码:

if (err instanceof MyError) {  
    // do some job
    console.log("MyError occurred")
}

目前看来一切都是那么顺其自然,那么直接去现代浏览器的Console或者Node Repl环境中执行这段代码,能够正常打印MyError occurred

MyError occurred

Babel/TypeScript 编译ES5

目前情况下,ES6代码应该由Babel(如果是TS用TypeScript)编译成ES5,这时会发现错误流程永远触发不了。例如:

var err = new MyError("this is a MyError")  
console.log(err instanceof MyError)  

输出

> false

而且若有其他类继承MyError 也将出现一样的结果。

如果拿普通的类来继承,例如:

class Animal {  
    constructor (name) {
        this.name = name
    }
}

class Bird extends Animal {  
    constructor (name) {
        super(name)
    }
}

var swallow = new Bird('swallow')

console.log(swallow instanceof Bird)  

则又会输出:

> true

MyError vs Bird inherits

从上图可以看出,Bird继承Animal类的行为是完全正常的,包括原型的引用,而Error的继承,其子类MyError实例的__proto__错误指向到了Error.prototype

那么来看看Babel编译的ES5代码如何实现继承的,代码做了一下简化:

var MyError = (function (_super) {  
    Object.setPrototypeOf(MyError, _super)
    function F() {this.constructor = MyError}
    F.prototype = _super.prototype
    MyError.prototype = new F()

    function MyError(msg) {
        var _this = _super.call(this, msg) || this;
        _this.message = msg;
        _this.name = "MyError";
        return _this;
    }
    return MyError;
}(Error));

继承实现的代码方式是一致的,_super参数换成Error为什么就不行了呢。

回到Error这个对象本身,它是JS中原生基本对象,它即是一个构造函数,也是一个普通函数。即我们创建一个Error实例可以通过如下两种方式:

var err1 = new Error('msg')  
var err2 = Error('msg')  

再回到编译的ES5代码,在子类构造函数中,第一行会调用父类构造函数:

var _this = _super.call(this, msg) || this  

_this结果也在构造函数尾部返回,当通过new操作符实例化子类时返回的实例就是这个_this的对象。

一个普通的父类构造函数,如Animal中,我们没有做任何return输出,作为构造函数使用时,就相当于return this,而我们又通过call绑定了this上下文为子类的this,那么子类构造函数中返回的就是子类的实例。

对于Error来说,其设计初衷就是可以作为普通函数调用,也就是其内部实现有return语句的,返回的正是Error实例,导致了_this的结果就是这个Error实例,并作为子类实例的this存在,这就导致了MyError子类的实例原型引用直接对接到Error上。

// Error构造函数可能的部分实现逻辑
function Error(msg){  
    if (!(this instanceof Error)) {
        return new Error(msg)
    }
    this.message = msg
    // more code
}

如何继承Error

我们想用ES6写代码,然后编译成ES5上线。但是不改变Babel编译方式的情况下,如何最便捷的实现目的呢?这里我们可以设计一个中间类,来改变构造函数中返回的结果,下面代码是extensible-error 包中的实现:

class ExtensibleError {  

}

function ExtendableErrorBuiltin(){  
    function ExtendableBuiltin(){
        Error.apply(this, arguments);

        // Set this.message
        Object.defineProperty(this, 'message', {
            configurable: true,
            enumerable: false,
            value: arguments.length ? String(arguments[0]) : ''
        })

        // Set this.name
        Object.defineProperty(this, 'name', {
            configurable: true,
            enumerable: false,
            value: this.constructor.name
        })

        if (typeof Error.captureStackTrace === 'function') {
            // Set this.stack
            Error.captureStackTrace(this, this.constructor)
        }
    }
    ExtendableBuiltin.prototype = Object.create(Error.prototype);
    Object.setPrototypeOf(ExtendableBuiltin, Error);

    return ExtendableBuiltin;
}

ExtensibleError = ExtendableErrorBuiltin(Error)  

要创建自定义错误类,可以:

class MyError extends ExtensibleError {  
  constructor (msg, extra) {
    super(msg)

    this.extra = extra
  }
}

看一下编译成ES5并执行的效果: MyError extensible