背景
在写业务代码时,前端与服务端约定了多个code,为了更好的辨识和使用Error,前端自定义了多种Error类,因此自定义了多个新的Error类,并且使用ES6的extend语法继承内置Error
class CustomError extends Error {
constructor ({ code, message }) {
super(message);
this.code = code;
}
}
遇到的问题
在项目里使用CustomError的过程中,出现了以下的情况
const error = new CustomError({ code: -1, message: 'custom error' })
console.log(error instanceof CustomError) // true
console.log(error instanceof Error) // true
console.log(error.code) // -1
console.log(error.message) // ''
??? message竟然消失了
解决之路
1.难道继承代码没写对?
创建一个HTML文件(保证环境一致),将源代码粘贴过去,使用Chrome打开,结果如下
const error = new CustomError({ code: -1, message: 'custom error' })
console.log(error instanceof CustomError) // true
console.log(error instanceof Error) // true
console.log(error.code) // -1
console.log(error.message) // 'custom error'
??? 结果如此,意味着代码本身是没有问题的,那项目里的message消失的原因是什么呢?
2.寻找原因
一步步分析,项目的代码和直接运行的demo区别在于项目代码因为考虑浏览器兼容性,使用了babel进行编译,那难道是babel编译得有问题吗?于是将代码直接复制到babel的网址上看编译后的文件(项目的代码使用了webpack打包,不是很好查看)
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var CustomError = function (_Error) {
_inherits(CustomError, _Error);
function CustomError(_ref) {
var code = _ref.code,
message = _ref.message;
_classCallCheck(this, CustomError);
var _this = _possibleConstructorReturn(this, (CustomError.__proto__ || Object.getPrototypeOf(CustomError)).call(this, message));
_this.code = code;
return _this;
}
return CustomError;
}(Error);
分析编译后的代码,new CustomError()返回的实例即代码中_this,这个_this取决于Error.call(this, message)是否返回function or object,此处便涉及到一个知识点:Error、Array等函数在设计上和其他函数有一个小区别,它们提供直接当普通function使用返回实例能力,即new Error(‘err’)与Error(‘err’)表现一致,均返回Error的实例,因此Error.call(this, message)返回了一个Error实例,所以new CustomError()最终得到的是一个Error实例。
测试:
const error = new CustomError({ code: -1, message: 'custom error' })
console.log(error instanceof CustomError) // false
console.log(error instanceof Error) // true
console.log(error.code) // -1
console.log(error.message) // 'custom error'
这个结果和项目中的结果更是不同,于是还是直接看项目build后的代码了
function _extendableBuiltin(cls) {
function ExtendableBuiltin() {
cls.apply(this, arguments);
}
ExtendableBuiltin.prototype = (0, _create2.default)(cls.prototype, {
constructor: {
value: cls,
enumerable: false,
writable: true,
configurable: true
}
});
if (_setPrototypeOf2.default) {
(0, _setPrototypeOf2.default)(ExtendableBuiltin, cls);
} else {
ExtendableBuiltin.__proto__ = cls;
}
return ExtendableBuiltin;
}
var BusinessError = function (_extendableBuiltin2) {
(0, _inherits3.default)(BusinessError, _extendableBuiltin2);
function BusinessError(_ref) {
var code = _ref.code,
message = _ref.message;
(0, _classCallCheck3.default)(this, BusinessError);
var _this = (0, _possibleConstructorReturn3.default)(this, (BusinessError.__proto__ || (0, _getPrototypeOf2.default)(BusinessError)).call(this, message));
_this.code = code;
return _this;
}
return BusinessError;
}(_extendableBuiltin(Error));
对比项目中编译后的代码与babel网站上编译的代码,可以发现项目中多了一个“_extendableBuiltin”,这个函数返回一个继承Error的类,而函数本身只是调用了cls.apply(this, arguments),没有返回值,因此new CustomError()返回的直接是this,又妄图使用superClass.call(this, message)来为this挂上message属性,而由于Error的函数中没有使用this,而是直接创建了一个实例返回,因此this上并没有挂上message,这就导致了我们项目里message丢失的问题。
解决
问题发现了,的确是babel编译引起的问题,于是网上搜了搜extendBuiltin,发现这是一个叫“babel-plugin-transform-builtin-extend”的babel plugin编译的结果,去到这个plugin的github,看到了一个配置项:“approximate”,额,好吧,这是为了兼容IE<=10的配置项,添加之后就会以Babel5的风格进行编译,而项目中使用了该插件,使用时也没有仔细查看说明,以致于出现了问题,考虑可以不兼容IE,便将该配置项去除,重新打包,可以看到打包后的代码结果如下:
function _extendableBuiltin(cls) {
function ExtendableBuiltin() {
var instance = (0, _construct2.default)(cls, (0, _from2.default)(arguments));
(0, _setPrototypeOf2.default)(instance, (0, _getPrototypeOf2.default)(this));
return instance;
}
// ...
return ExtendableBuiltin;
}
可以看到现在的_extendableBuiltin中返回的ExtendableBuiltin在调用时,第一步通过construct创建一个instance(使用Reflect.construct()),第二步将instance的__ptoto__设置为this,第三步返回instance,如此便可解决以上出现的系列问题。(IE<=10下,不支持Object.setPrototypeOf(),也不支持直接对__proto__的修改,因此第二步不起作用;第一步中默认使用的construct为Reflect.construct,直接不支持IE系列~~,想在IE下玩耍还需要打polyfill以及其他一些操作~)
扩展
在babel7中,继承内置函数使用的插件推荐“”,该插件内置在了babel-core中,开发者不需要另行安装,有兴趣的小伙伴可以研究两个插件打包后的代码有什么区别,为什么会换?
参考文献:
- https://hacks.mozilla.org/2015/08/es6-in-depth-subclassing/
- http://speakingjs.com/es5/ch28.html
- https://github.com/babel/babel/issues/4480
- https://github.com/loganfsmyth/babel-plugin-transform-builtin-extend