ES6 class
在ES6版本之前,JavaScript语言并没有传统面向对象语言的class写法,ES6发布之后,Babel迅速跟进,广大开发者也很快喜欢上ES6带来的新的编程体验。
当然,在这门“混乱”而又精妙的语言中,许多每天出现我们视野中的东西却常常被我们忽略。
对于ES6语法,考虑到浏览器的兼容性问题,我们还是要把代码转换为ES5版本运行。然而,之前的ES版本为什么能模仿ES6的诸多特性,比如class与继承,super,static?JavaScript又做了哪些改变以应对这些新角色?本文将对class实例构造,class继承关系,super关键字,static关键字的运行机制进行探索。
水平有限,文中若有引起困惑或错误之处,还望指出。
class实例构造
class基本样例
基本而言,ES6 class形式如下:
class Whatever{
}
当然,还可以有constructor方法。
class Whatever{
constructor(){
this.name = 'hahaha';
}
}
请看ES5对应版本:
function Whatever{
this.name = 'hahaha';
}
new干了什么
可知,constructor相当于以前在构造函数里的行为。而对于ES5构造函数而言,在被new调用的时候,大体上进行了下面四步:
- 新建对象var _this = {};
- this的[[prototype]]指向构造函数的prototype,即_this.__proto_ = Constructor.prototype
- 改变Constructor的this到_this并执行Constructor,即Constructor.apply(_this,agrs);得到构造好的_this对象
- 判断Constructor的返回值,若返回值不为引用类型,则返回_this,否则返回改引用对象
所以,构造函数的实例会继承挂载在prototype上的方法,在ES6 calss中,我们这样写会把方法挂载在class的prototype:
class Whatever{
//...
methodA(){
//...
}
}
对应ES5写法:
Whatever.prototype = function methodA(){
//...
}
class继承关系
原型语言基本特点
在基于原型的语言,有以下四个特点:
- 一切皆为对象(js中除了对象还有基本类型,函数式第一等对象)
- 对象皆是从其他对象复制而来(在JS对象世界中,万物始于Object.prototype这颗蛋)
- 对象会记住它的原型(在JS中对象的__proto__属性指向它的原型)
- 调用对象本身没有的属性/方法时,对象会尝试委托它的原型
看到这,大家应该明白了,为什么挂载在Constructor.prototype的方法会被实例“继承”!
在ES6 class中,继承关系还是由[[prototype]]连维持,即:
Child.prototype.__proto__ === Parent.prototype;
Child.__proto__ === Parent;
childObject.__proto === Child.prototype;
当箭头函数与class碰撞
ES6的箭头函数,一出身便深受众人喜爱,因为它解决了令人头疼的函数执行时动态this指向的“问题”(为什么加引号?因为有时候我们有时确实需要动态this带来的巨大便利)。箭头函数中this绑定在词法作用域,即它定义的地方:
//ES6:
const funcArrow = () => {
//your code
}
//ES5:
var _this = this;
var funcArrow = function(){
this = _this;
//your code
}
有的童鞋可能会想到了,既然js中继承和this的关系这么大,在calss中采用词法绑定this的箭头函数,会有怎么样呢?
我们来瞧瞧。
class WhateverArrow{
//
methodArrow = () => {
//...
}
}
这种写法会与上文中写法有何区别?
class WhateverNormal{
//
methodNormal() {
//...
}
}
我们在chrome环境下运行一下,看看这两种构造函数的prototype有何区别:
WhateverArrow.prototype打印结果:
constructor: class Whatever1
__proto__: Object
WhateverNormal.prototype打印结果:
constructor: class Whatever2
methodNormal: ƒ methodNormal()
__proto__: Object
结合上文中关于原型的论述,仔细品味这两者的差别,最好手动尝试一下。
方法与函数类型属性
我们称func(){}的形式为“方法”,而methodArrow = () =>:any为属性!方法会被挂载在prototype,在属性不会。箭头函数methodArrow属性会在构造函数里赋值给this:
this.methodArrow = function methodArrow(){
this = _this;
//any code
}
在实例调用methodArrow时,调用的是自己的methodArrow,而非委托calss WhateverArrow.prototype上的方法,而这个箭头函数中this的指向,Babel或许能给我们一些启示:
var WhateverArrow = function WhateverArrow() {
var _this = this;
_classCallCheck(this, WhateverArrow);
_defineProperty(this, "methodArrow", function () {
consoe.log(_this);
});
};
遇见extends,super与[[HomeObject]]
让我们extends一下
当我们谈论继承时,往往指两种:
- 对象实例继承自一个类(构造函数)
- 子类继承父类
上文中我们探讨了第一种,现在,请把注意力转向第二种。
考虑下方代码:
class Parent {
constructor(){
this.tag = 'A';
this.name = 'parent name'
}
methodA(){
console.log('methodA in Parent')
}
methodB(){
console.log(this.name);
}
}
class Child extends Parent{
constructor(){
super();
//调用super()之后才用引用this
this.name = 'child name'
}
methodA(){
super.methodA();
console.log('methodA in Child')
}
}
const c1 = new Child();
c1.methodA();//methodA in Parent // methodA in Child
我们通过extends连接了两个class,标明他们是“父子关系”的类,子类中方法会屏蔽掉父类中同名方法,与Java中多态特性不同,这里的方法参数数量并不影响“是否同一种方法”的判定。
在Child的constructor中,必须在调用super()之后才能调用this,否则将会因this为undefined而报错。其中缘由,简单来说就是执行new操作时,Child的_this来自于调用Parent的constructor,若不调用super(),_this将为undefined。对这个问题感兴趣的同学可以自行操作试试,并结合Babel的转换结果,进行思考。
super来自何方?[[HomeObject]]为何物?
super干了什么
super可以让我们在子类中借用父类的属性和方法。
methodA(){
super.methodA();
console.log('methodA in Child')
}
super关键词真是一个增进父子情的天才创意!
值得注意的是,子类中methodA调用super.methodA()时候,super.methodA中的this绑定到了子类实例。
super来自何方?如何请到super这位大仙?
用的舒服之后,我们有必要想一想,Child.prototype.methodA中的super是如何找到Parent.prototype.methodA的?
我们知道:
Child.prototype.__proto__ === Parent.prototype;
cs.__proto__ === Child.prototype;
c1.methodA();
当c1.methodA()执行时,methodA中this指向c1,难道通过多少人爱就有多少人恨的this?
仔细想想,如果是这样(通过this找),考虑如下代码:
//以下代码删除了当前话题无关行
class GrandFather{
methodA(){
console.log('methodA in GrandFather')
}
}
class Parent extends GrandFather{
methodA(){
super.methodA();
console.log('methodA in Parent')
}
}
class Child extends Parent{
methodA(){
super.methodA();
console.log('methodA in Child')
}
}
想想我们现在是执行引擎,我们通过this找到了c1,然后通过原型找到了Child.prototype.methodA;
在Child.prototype.methodA中我们遇见了super.methodA();
现在我们要去找super,即Parent。
我们通过this.__proto__.__proto__methodA找到了Parent.prototype.methodA;
对于Parent.prototype.methodA来说,也要像对待c1一样走这个方式找,即在Parent..prototype.methodA中通过this找其原型。
这时候问题来了,运行到Parent.prototype.methodA时,该方法中的this指向的还是c1。
这岂不是死循环了?
显然,想通过this找super,只会鬼打墙。
[[HomeObject]]横空出世
为了应对super,js引擎干脆就让方法(注意,是方法,不是属性)在创建时硬绑定上[[HomeObject]]属性,指向它所属的对象!
显然,Child中methodA的[[HomeObject]]绑定了Child.prototype,Parent中methodA的[[HomeObject]]绑定了Parent.prototype。
这时候,根据[[HomeObject]],可以准确无误地找到super!
而在Babel转为ES5时,是通过硬编码的形式,解决了对super的引用,思路也一样,硬绑定当前方法所属对象(对象或者函数):
//babel转码ES5节选
_createClass(Parent, [{
key: "methodA",
value: function methodA() {
//此处就是对super.methodA()所做的转换,同样是硬绑定思路
_get(_getPrototypeOf(Parent.prototype), "methodA", this).call(this);
console.log('methodA in Parent');
}
}]);
注意属性与方法的差别:
var obj1 = {
__proto__:SomePrototype,
methodQ(){ //methodQ绑定了[[HomeObject]]->obj1,调用super
super.someMethod();
}
}
var obj2 = {
__proto__:SomePrototype,
methodQ:function(){ //methodQ不绑定任何[[HomeObject]]
super.someMethod();//Syntax Eroor!语法错误,super不允许在对象的非方法中调用
}
}
箭头函数再袭super
结合前文中关于class内部箭头函数的谈论,有个问题不得不引起我们思考:class中的箭头函数里的super指向哪里?
考虑如下代码:
class Parent{
methodA(){
console.log('methodA in Parent')
}
}
class Child extends Parent{
methodA = () => {
super.methodA();
console.log('methodA in Child')
}
}
const c1 = new Child();
c1.methodA();
输出为:
methodA in Parent
methodA in Child
似乎没什么意外。我们需要更新异步,把Parent的methodA方法改为箭头函数:
class Parent{
methodA = () => {
console.log('methodA in Parent')
}
}
class Child extends Parent{
methodA = () => {
super.methodA();
console.log('methodA in Child')
}
}
const c1 = new Child();
c1.methodA();
很抱歉,人见人恨得异常发生了:
Uncaught TypeError: (intermediate value).methodA is not a function
at Child.methodA
如何把Child中的methodA改为普通方法函数呢?
class Parent{
methodA = () => {
console.log('methodA in Parent')
}
}
class Child extends Parent{
methodA () {
super.methodA();
console.log('methodA in Child')
}
}
const c1 = new Child();
c1.methodA();
输出:
methodA in Parent
//并没有打印methodA in Child
以上几种结果产生的原因请结合前几章节细致品味,你会有所收获的。
不容忽视的static
static的表现
简单来说,static关键词标志了一个挂载在class本身的属性或方法,我们可以通过ClassName.staticMethod访问到。
class Child{
static name = '7788';
static methodA () {
console.log('static methodA in Child')
}
}
Child.name;//7788;
Child.methodA();//static methodA in Child
static如何传给子类
因为Child本身的[[prototype]]指向了Parent,即Child.__proto__===Parent 所以,static可以被子类继承:
class Parent{
static methodA () {
console.log('static methodA in Parent')
}
}
class Child extends Parent{
}
Child.methodA();//static methodA in Parent
static方法中访问super
class Parent{
static methodA () {
console.log('static methodA in Parent')
}
}
class Child extends Parent{
static methodA () {
super.methodA()
console.log('static methodA in Child')
}
}
Child.methodA();
//输出:
//static methodA in Parent
// static methodA in Child
结语
JS是门神奇的语言,神奇到很多人往往会用JS但是不会JS(...hh)。作为一门热门且不断改进中的语言,由于跟随时代和历史遗留等方面的因素,它有很多令人迷惑的地方。
在我们每天面对的一些特性中,我们很容易忽视其中机理。就算哪天觉得自己明白了,过一段时间可能又遇到别的问题,突然觉得自己懂得还是太少(还是太年轻)。然后刨根问底的搞明白,过一段时间可能又。。。或者研究JS的历程就是这样螺旋式的进步吧。
感谢Babel,她真的对我们理解JS一些特性的运行机理非常有用,因为Babel对JS吃的真的很透彻(...)。她对ES6的“翻译”,可以帮助我们对ES6新特性以及往前版本的JS的理解。
行文匆忙,难免有错漏之处,欢迎指出。
祝大家身体健康,BUG越来越少。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。