如果以前问我ES5
的继承和ES6
的继承有什么区别,我肯定会自信的说没有区别,不过是语法糖而已,充其量也就是写法有区别,然而当初我会伪装思考一下,而后说尽管只是语法糖,但也是有点小区别的,那么具体有什么区别呢,不要走开,下文更精彩!
本文会先回顾一下ES5
的寄生组合式继承的实现,而后再看一下ES6
的写法,最初依据Babel
的编译后果来看一下到底有什么区别。
ES5:寄生组合式继承
js
有很多种继承形式,比方大家耳熟能详的原型链继承
、结构继承
、组合继承
、寄生继承
等,然而这些或多或少都有一些不足之处,所以笔者认为咱们只有记住一种就能够了,那就是寄生组合式继承
。
首先要明确继承到底要继承些什么货色,一共有三局部,一是实例属性/办法、二是原型属性/办法、三是动态属性/办法,咱们别离来看。
先来看一下咱们要继承的父类的函数:
// 父类function Sup(name) { this.name = name// 实例属性}Sup.type = '午'// 动态属性// 静态方法Sup.sleep = function () { console.log(`我在睡${this.type}觉`)}// 实例办法Sup.prototype.say = function() { console.log('我叫 ' + this.name)}
继承实例属性/办法
要继承实例属性/办法,显著要执行一下Sup
函数才行,并且要批改它的this
指向,这应用call
、apply
办法都行:
// 子类function Sub(name, age) { // 继承父类的实例属性 Sup.call(this, name) // 本人的实例属性 this.age = age}
能这么做的原理又是另外一道经典面试题:new操作符都做了什么
,很简略,就4
点:
1.创立一个空对象
2.把该对象的__proto__
属性指向Sub.prototype
3.让构造函数里的this
指向新对象,而后执行构造函数,
4.返回该对象
所以Sup.call(this)
的this
指的就是这个新创建的对象,那么就会把父类的实例属性/办法都增加到该对象上。
继承原型属性/办法
咱们都晓得如果一个对象它自身没有某个办法,那么会去它构造函数的原型对象上,也就是__proto__
指向的对象上查找,如果还没找到,那么会去构造函数原型对象的__proto__
上查找,这样一层一层往上,也就是传说中的原型链,所以Sub
的实例想要能拜访到Sup
的原型办法,就须要把Sub.prototype
和Sup.prototype
关联起来,这有几种办法:
1.应用Object.create
Sub.prototype = Object.create(Sup.prototype)Sub.prototype.constructor = Sub
2.应用__proto__
Sub.prototype.__proto__ = Sup.prototype
3.借用两头函数
function Fn() {}Fn.prototype = Sup.prototypeSub.prototype = new Fn()Sub.prototype.constructor = Sub
以上三种办法都能够,咱们再来笼罩一下继承到的Say
办法,而后在该办法外面再调用父类原型上的say
办法:
Sub.prototype.say = function () { console.log('你好') // 调用父类的该原型办法 // this.__proto__ === Sub.prototype、Sub.prototype.__proto__ === Sup.prototype this.__proto__.__proto__.say.call(this) console.log(`往年${this.age}岁`)}
继承动态属性/办法
也就是继承Sup
函数自身的属性和办法,这个很简略,遍历一下父类本身的可枚举属性,而后增加到子类上即可:
Object.keys(Sup).forEach((prop) => { Sub[prop] = Sup[prop]})
ES6:应用class继承
接下来咱们应用ES6
的class
关键字来实现下面的例子:
// 父类class Sup { constructor(name) { this.name = name } say() { console.log('我叫 ' + this.name) } static sleep() { console.log(`我在睡${this.type}觉`) }}// static只能设置静态方法,不能设置动态属性,所以须要自行添加到Sup类上Sup.type = '午'// 另外,原型属性也不能在class外面设置,须要手动设置到prototype上,比方Sup.prototype.xxx = 'xxx'// 子类,继承父类class Sub extends Sup { constructor(name, age) { super(name) this.age = age } say() { console.log('你好') super.say() console.log(`往年${this.age}岁`) }}Sub.type = '懒'
能够看到一样的成果,应用class
会简洁明了很多,接下来咱们应用babel
来把这段代码编译回ES5
的语法,看看和咱们写的有什么不一样,因为编译完的代码有200多行,所以不能一次全副贴上来,咱们先从父类开始看:
编译后的父类
// 父类var Sup = (function () { function Sup(name) { _classCallCheck(this, Sup); this.name = name; } _createClass( Sup, [ { key: "say", value: function say() { console.log("我叫 " + this.name); }, }, ], [ { key: "sleep", value: function sleep() { console.log("\u6211\u5728\u7761".concat(this.type, "\u89C9")); }, }, ] ); return Sup;})(); // static只能设置静态方法,不能设置动态属性Sup.type = "午"; // 子类,继承父类// 如果咱们之前通过Sup.prototype.xxx = 'xxx'设置了原型属性,那么跟动态属性一样,编译后没有区别,也是这么设置的
能够看到是个自执行函数,外面定义了一个Sup
函数,Sup
外面先调用了一个_classCallCheck(this, Sup)
函数,咱们转到这个函数看看:
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); }}
instanceof
运算符是用来检测左边函数的prototype
属性是否呈现在右边的对象的原型链上,简略说能够判断某个对象是否是某个构造函数的实例,能够看到如果不是的话就抛错了,错误信息是不能把一个类当做函数调用
,这里咱们就发现第一个区别了:
区别1:ES5里的构造函数就是一个一般的函数,能够应用new调用,也能够间接调用,而ES6的class不能当做一般函数间接调用,必须应用new操作符调用
持续看自执行函数,接下来调用了一个_createClass
办法:
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor;}
该办法接管三个参数,别离是构造函数、原型办法、静态方法(留神不蕴含原型属性和动态属性),前面两个都是数组,数组外面每一项代表一个办法对象,不论是实例办法还是原型办法,都是通过_defineProperties
办法设置,先来看该办法:
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; // 设置该属性是否可枚举,设为false则for..in、Object.keys遍历不到该属性 descriptor.enumerable = descriptor.enumerable || false; // 默认可配置,即能批改和删除该属性 descriptor.configurable = true; // 设为true时该属性的值能被赋值运算符扭转 if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); }}
能够看到它是通过Object.defineProperty
办法来设置原型办法和静态方法,而且enumerable
默认为false
,这就来到了第二个区别:
区别2:ES5的原型办法和静态方法默认是可枚举的,而class的默认不可枚举,如果想要获取不可枚举的属性能够应用Object.getOwnPropertyNames办法
接下来看子类编译后的代码:
编译后的子类
// 子类,继承父类var Sub = (function (_Sup) { _inherits(Sub, _Sup); var _super = _createSuper(Sub); function Sub(name, age) { var _this; _classCallCheck(this, Sub); _this = _super.call(this, name); _this.age = age; return _this; } _createClass(Sub, [ { key: "say", value: function say() { console.log("你好"); _get(_getPrototypeOf(Sub.prototype), "say", this).call(this); console.log("\u4ECA\u5E74".concat(this.age, "\u5C81")); } } ]); return Sub;})(Sup);Sub.type = "懒";
同样也是一个自执行办法,把要继承的父类构造函数作为参数传进去了,进来先调用了_inherits(Sub, _Sup)
办法,尽管Sub
函数是在前面定义的,然而函数申明是存在晋升的,所以这里是能够失常拜访到的:
function _inherits(subClass, superClass) { // 被继承对象的必须是一个函数或null 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);}
这个办法先查看了父类是否非法,而后通过Object.create
办法设置了子类的原型,这个和咱们之前的写法是一样的,只是明天我才发现Object.create
竟然还有第二个参数,第二个参数必须是一个对象,对象的自有可枚举属性(即其本身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象增加指定的属性值和对应的属性描述符。
这个办法的最初为咱们揭晓了第三个区别:
区别3:子类能够间接通过
__proto__
找到父类,而ES5是指向Function.prototype
:ES6:
Sub.__proto__ === Sup
ES5:
Sub.__proto__ === Function.prototype
为啥会这样呢,看看_setPrototypeOf
办法做了啥就晓得了:
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p);}
能够看到这个办法把Sub.__proto__
设置为了Sup
,这样同时也实现了静态方法和属性的继承,因为函数也是对象,本身没有的属性和办法也会沿着__proto__
链查找。
_inherits
办法过后紧接着调用了一个_createSuper(Sub)
办法,拉进去看看:
function _createSuper(Derived) { return function _createSuperInternal() { // ... };}
这个函数接管子类构造函数,而后返回了一个新函数,咱们先跳到前面的子类构造函数的定义:
function Sub(name, age) { var _this; // 查看是否当做一般函数调用,是的话抛错 _classCallCheck(this, Sub); _this = _super.call(this, name); _this.age = age; return _this;}
同样是先查看了一下是否是应用new
调用,而后咱们发现这个函数返回了一个_this
,后面介绍了new
操作符都做了什么,咱们晓得会隐式创立一个对象,并且会把函数内的this
指向该对象,如果没有显式的指定构造函数返回什么,那么就会默认返回这个新创建的对象,而这里显然是手动指定了要返回的对象,而这个_this
来自于_super
函数的执行后果,_super
就是后面_createSuper
返回的新函数:
function _createSuper(Derived) { // _isNativeReflectConstruct会查看Reflect.construct办法是否可用 var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { // _getPrototypeOf办法用来获取Derived的原型,也就是Derived.__proto__ var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { // NewTarget === Sub var NewTarget = _getPrototypeOf(this).constructor; // Reflect.construct的操作能够简略了解为:result = new Super(...arguments),第三个参数如果传了则作为新创建对象的构造函数,也就是result.__proto__ === NewTarget.prototype,否则默认为Super.prototype result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); };}
Super
代表的是Sub.__proto__
,依据后面的继承操作,咱们晓得子类的__proto__
指向了父类,也就是Sup
,这里会优先应用Reflect.construct
办法,相当于创立了一个父类的实例,并且这个实例的__proto__
又指回了Sub.prototype
,不得不说这个api
真是神奇。
咱们就不思考降级状况了,那么最初会返回这个父类的实例对象。
回到Sub
构造函数,_this
指向的就是这个通过父类创立的实例对象,为什么要这么做呢,这其实就是第四个区别了,也是最重要的区别:
区别4:ES5的继承,本质是先发明子类的实例对象this
,而后再执行父类的构造函数给它增加实例办法和属性(不执行也无所谓)。而ES6的继承机制齐全不同,本质是先发明父类的实例对象this
(当然它的__proto__
指向的是子类的prototype
),而后再用子类的构造函数批改this
。
这就是为啥应用class
继承在constructor
函数里必须调用super
,因为子类压根没有本人的this
,另外不能在super
执行前拜访this
的起因也很显著了,因为调用了super
后,this
才有值。
子类自执行函数的最初一部分也是给它设置原型办法和静态方法,这个后面讲过了,咱们重点看一下实例办法编译后的后果:
function say() { console.log("你好"); _get(_getPrototypeOf(Sub.prototype), "say", this).call(this); console.log("\u4ECA\u5E74".concat(this.age, "\u5C81"));}
猜你们也忘了编译前的原函数是啥样的了,请看:
say() { console.log('你好') super.say() console.log(`往年${this.age}岁`)}
在ES6
的class
里super
有两种含意,当做函数调用的话它代表父类的构造函数,只能在constructor
外面调用,当做对象应用时它指向父类的原型对象,所以_get(_getPrototypeOf(Sub.prototype), "say", this).call(this)
这行大略相当于Sub.prototype.__proto__.say.call(this)
,跟咱们最开始写的ES5
版本也差不多,然而显然在class
的语法要简略很多。
到此,编译后的代码咱们就剖析的差不多了,不过其实还有一个区别不晓得大家有没有发现,那就是为啥要应用自执行函数,一当然是为了封装一些变量,二其实是因为第五个区别:
区别5:class不存在变量晋升,所以父类必须在子类之前定义
不信你把父类放到子类前面试试,不出意外会报错,你可能会感觉间接应用函数表达式也能够达到这样的成果,非也:
// 会报错var Sub = function(){ Sup.call(this) }new Sub()var Sup = function(){}// 不会报错var Sub = function(){ Sup.call(this) }var Sup = function(){}new Sub()
然而Babel
编译后的无论你在哪里实例化子类,只有父类在它之后申明都会报错。
总结
本文通过剖析Babel
编译后的代码来总结了ES5
和ES6
继承的5个区别,可能还有一些其余的,有趣味能够自行理解。
对于class
的详细信息能够看这篇继承class继承。
示例代码在https://github.com/wanglin2/es5-es5-inherit-example。