这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战
前言
ES5
中有6
种实现继承的模式,分别是:原型链继承、盗用构造函数继承、组合继承、原型式继承、寄生式继承以及寄生组合式继承,关于这6
种继承方式,掘金上已经有很多文章介绍,这里就不在赘述,整体看下来,这六种继承方式中,寄生组合式继承是最优的继承方式。ES6
提供了继承的关键字 extends
,虽然Class
本质上只是一种语法糖,但是extends
的工作原理与ES5
的继承却有很大不同,从而导致继承不同类型的对象所变现出的差异。
内置对象的继承
自定义对象ES5
、ES6
的继承行为都表现的一致,来看看内置对象的继承。
内置对象是指由js
内置的构造函数创建的对象,通常是一些数据结构,主要有Array
、Map
、Object
、Date
等等。在ES5
中,这些对象是无法被继承的,举个栗子,我们使用寄生组合继承来派生出一个Array
的子类看看:
function _inherit(subClass,superClass) {
subClass.prototype = Object.create(superClass.prototype,{
constructor: {
value: subClass,
writable: true,
configurable: true,
enumerable: true
}
});
}
function _Array() {
Array.apply(this, arguments);
}
_inherit(_Array,Array)
let myArr = new _Array(1,2,3)
console.log(myArr instanceof Array) // true
console.log(myArr.length)// 0
console.log(Array.isArray(myArr))// false
复制代码
myArr
确实是Array
的一个子类,并且没有新加任何属性,但是,这个类的length
行为与Array
完全不一致,传入的数据被忽略了,可以控制台运行上述代码看看。但是后续调用push
方法依旧可以往里面添加数据,因为push
操作数据的同时也会更新length
属性,原本原型链顶层没有length
的,但是push
新建了一个length
。
再来看看ES6
的继承实现:
class _Array extends Array {
constructor(...args) {
super(...args);
}
}
let myArr = new _Array(1,2,3)
console.log(myArr instanceof Array) // true
console.log(myArr.length)// 3
console.log(Array.isArray(myArr))// true
复制代码
子类myArr
与父类Array
的行为完全一致,ES6能够实现对内置对象的继承。
这只是继承Array
,没有添加属性也没有报错,但是如果试图用ES5
的方式去继承Map
、Set
这些ES6的数据结构,在创建实例时会报Constructor xxx requires 'new'
的错误,继承Date
之类的对象,可以创建实例,但是当你调用其方法时,则会报this is not a xxx object
。
差异的原因
是什么导致了这种行为差异?根本原因是子类无法获得内置对象构造函数的内部属性。在寄生组合继承中,获得父类的内部属性是通过call/apply
绑定this
这种鸠占鹊巢的方式来获得,但是内置对象的构造函数会忽略apply / call
方法传入的this
,也就是说,原生构造函数的this
无法绑定,导致拿不到内部属性。
再来看看它们继承的过程,寄生组合继承先新建子类的实例对象this
,再将父类的属性添加到子类上,而ES6
的继承是先新建父类的实例对象this
,然后再用子类的构造函数修饰this
,使得父类的所有行为都可以继承,这也是为什么ES6
继承一定要在子类构造函数中先调用super()
的原因。
super
代表了父类的构造函数,但是返回的是子类的实例,即super
内部的this
指向的是子类的实例,即super
类似于A.prototype.constructor.call(B)
。我个人这么理解,ES5
的继承获取父类内部属性是通过call/apply
这种强制改变父类构造函数的运行时来获得,而super
则是相当于拿到父类构造函数的函数体放到当前运行时执行,使得父类的所有属性都可以继承。
Babel如何转译
Babel
能够将高级js
语法转译为ES5
,从而帮助我们解决浏览器兼容问题,那么它是如何将Class...extends
转译为ES5
语法的呢。还是以Array
的子类为例:Babel在线转译
class _Array extends Array{
constructor() {
super();
}
}
复制代码
转译后???
// 确保构造函数只能作为构造函数使用而不能作为普通函数
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
// 绑定原型链
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true }
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
// 创建父类对象实例,类似于super()
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
// 父类构造函数包装器
function _wrapNativeSuper(Class) {
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
return _setPrototypeOf(Wrapper, Class);
};
return _wrapNativeSuper(Class);
}
function _construct(Parent, args, Class) {
if (_isNativeReflectConstruct()) {
_construct = Reflect.construct;
} else {
_construct = function _construct(Parent, args, Class) {
var a = [null];
a.push.apply(a, args);
var Constructor = Function.bind.apply(Parent, a);
var instance = new Constructor();
if (Class) _setPrototypeOf(instance, Class.prototype);
return instance;
};
}
return _construct.apply(null, arguments);
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}
// 判断是否是内置方法
function _isNativeFunction(fn) {
return Function.toString.call(fn).indexOf("[native code]") !== -1;
}
// Object.setPrototypeOf特性检测
function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
// Object.getPrototypeOf 特性检测
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
// 核心
var _Array = /*#__PURE__*/ (function (_Array2) {
"use strict";
_inherits(_Array, _Array2);
var _super = _createSuper(_Array);
function _Array() {
_classCallCheck(this, _Array);
return _super.call(this);
}
return _Array;
})(/*#__PURE__*/ _wrapNativeSuper(Array));//对父类构造函数做了一层包装
复制代码
转译后就是这么大一坨,不过大部分都是特性检测的代码,我们先把它拿去测试一下,看下是否能达到我们想要的效果,直接复制转译后的代码到浏览器,然后创建一个_Array
实例。
let myArr = new _Array(1,2,3)
console.log(myArr instanceof Array) // true
console.log(myArr.length)// 0
console.log(Array.isArray(myArr))// true
复制代码
emmm,Array.isArray
判断子类是个数组对象了,但是传入的数据依旧被忽略了。再来看看继承Map
这种构造函数必须通过new调用的对象转译后能不能使用。这里就不贴完整转译的代码,只讨论结果,可以去在线转译看看。
/**其他转译代码忽略**/
let _Map = /*#__PURE__*/ (function (_Map2) {
"use strict";
_inherits(_Map, _Map2);
var _super = _createSuper(_Map);
function _Map() {
_classCallCheck(this, _Map);
return _super.call(this);
}
return _Map;
})(/*#__PURE__*/ _wrapNativeSuper(Map));
let map = new _Map([["name","jaylenl"]])
console.log(map.has("name"))// false
map.set("name","jaylenl")
console.log(map.has("name"))// true
复制代码
转移后可以new
调用了,但是和_Array
一样,传入构造函数的参数依旧会被忽略。看来Babel转译仍旧无法解决根本问题:子类无法获取父类的内部属性。即ES5
层面就不可能完全实现内置函数的继承。
我们来详细看看Babel是怎样基于ES5实现继承的。
var _Array = /*#__PURE__*/ (function (_Array2) {
"use strict";
// 原型链继承
_inherits(_Array, _Array2);
//
var _super = _createSuper(_Array);
function _Array() {
// 验证是否是 _Array 构造出来的 this
_classCallCheck(this, _Array);
return _super.call(this);
}
return _Array;
})(/*#__PURE__*/ _wrapNativeSuper(Array));
复制代码
从上面的核心代码中看出,Babel也是采用寄生组合继承的模式,也证明了这种模式是较优的继承解决方案,不过它做了一些优化,我们重点看_wrapNativeSuper
、_inherits
和_createSuper
。
_wrapNativeSuper
function _wrapNativeSuper(Class) {
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
return _setPrototypeOf(Wrapper, Class);
};
return _wrapNativeSuper(Class);
}
复制代码
在进行继承前,会先对父类用此函数进行包装成一个Wrapper
,这个包装对象会绑定父类的原型链以及父类构造函数的自身的所有属性和方法。后续继承会以这个包装对象为基础。这样继承得到的子类除了拿到父类原型链上的属性和方法也能获得父类的静态方法和属性。
_inherits()
function _inherits(subClass, superClass) {
// 构造函数合法性检测
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 将子类的原型链与父类的原型链绑定,使得子类能获得父类原型的方法和属性
subClass.prototype = Object.create(superClass && superClass.prototype, {
// constructor绑定回子类构造函数
constructor: { value: subClass, writable: true, configurable: true }
});
// _setPrototypeOf相当于Obeject.setPrototypeOf,但这是es6新增的方法。
// 用于设置一个指定的对象的原型,即内部[[Prototype]]属性)到另一个对象上
if (superClass) _setPrototypeOf(subClass, superClass);
}
复制代码
理解上面代码得先了解Object.create
和Obeject.setPrototypeOf
,这里就不介绍了,请看MDN
,代码做的事情就是改变子类构造函数的原型prototype
,改变的方式就是用一个基于父类原型链的创建的对象进行替换,由于函数prototype
下有一个constructor
属性来指向自身,因此再为这个对象新建一个指向子类构造函数的constructor
属性。其实这就是原型链继承的内容,很好理解。
_createSuper
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
// 获取子类原型链prototype,由_inherits()从父类那得来的,相当于父类prototype的深克隆。
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
// 盗用构造函数继承,绑定父类的内部属性。
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
复制代码
这一步利用构造函数继承来获得父类构造函数的内部属性,也是使用apply
传递this
来实现,对于内置对象这是无效的,这也就说明了为什么子类构造函数的参数无效的原因,正如上面所说,传递this
这种方式会被内置对象构造函数忽略,因此Babel依旧不能完全实现内置对象的继承。
T.J. Crowder
在 How does Babel.js create compile a class declaration into ES2015? 中谈到 Babel
是如何将class
转化为 ES5
兼容代码时谈到了几点:
ES6
中class
的constructor
变成构造函数;- 所有非构造函数、非静态方法都成为原型方法;
- 静态方法会成为构造函数的属性,其他属性保持不变;
- 创建对象以放置在派生构造函数的
prototype
属性上是通过object.create(Base.prototype)
而不是new Base()
完成的。 constructor
调用基类是第一步操作;ES5
中对应ES6
的super()
的写法是Base.prototype.constructor.call(this);
,这种操作不仅繁琐而且容易出错。
总结
ES6
与ES5
继承的区别:
ES5
先创建子类实例this
,再实例化父类并添加到子类this
中。ES6
先创建父类实例this
,再实例化子类this
用来修饰父类实例。
对于自定义对象的继承,ES5
、ES6
都能很好解决,但对于内置对象,ES5不可能完全实现,ES6
的extends
才是终极解决方案,即便Babel
有对class...extends
语法的转译,但终究不能解决子类无法获得内置对象构造函数的内部属性这个根本问题,它只是在运行时层面使用现有语言特性对高级特性的一种 Polyfill
。
但毕竟转译成ES5
是为了兼容低版本的浏览器,而继承内置对象的使用场景很少见,例如Web Components
就需要继承内置DOM
对象,但这种特性本身就是为现代浏览器设计的。
创作不易,如果这篇文章对你有帮助,欢迎点赞!文中如果有错误,感谢评论区斧正。