资讯专栏INFORMATION COLUMN

ES6 class继承与super关键词深入探索

jubincn / 3096人阅读

摘要:请看对应版本干了什么可知,相当于以前在构造函数里的行为。这种写法会与上文中写法有何区别我们在环境下运行一下,看看这两种构造函数的有何区别打印结果打印结果结合上文中关于原型的论述,仔细品味这两者的差别,最好手动尝试一下。

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越来越少。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106902.html

相关文章

  • 从-1开始的ES6探索之旅02:小伙子,你对象咋来的?续篇 - 你的对象班(class)里来的?

    摘要:这是因为子类没有自己的对象,而是继承父类的对象,然后对其进行加工。 温馨提示:作者的爬坑记录,对你等大神完全没有价值,别在我这浪费生命温馨提示-续:你们要非得看,我也拦不住,但是至少得准备个支持ES6的Chrome浏览器吧?温馨提示-再续:ES6简直了,放着不用简直令人发指! 书接上回,即便是程序员,也还是能够通过自己的努力辛辛苦苦找到合适对象的,见前文《javascript对象不完全...

    incredible 评论0 收藏0
  • JavaScript是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换

    摘要:下面是用实现转成抽象语法树如下还支持继承以下是转换结果最终的结果还是代码,其中包含库中的一些函数。可以使用新的易于使用的类定义,但是它仍然会创建构造函数和分配原型。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 15 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是...

    PrototypeZ 评论0 收藏0
  • 探索 proto & prototype 继承之间的关系

    摘要:而和的存在就是为了建立这种子类与父类间的联系。创建一个基本对象建立新对象与原型我把它理解为类之间的连接执行构造函数小结可以理解为类,也就是存储一类事物的基本信息。原型原型链和继承之间的关系。 原型 原型的背景 首先,你应该知道javascript是一门面向对象语言。 是对象,就具有继承性。 继承性,就是子类自动共享父类的数据结构和方法机制。 而prototype 和 __proto__...

    dockerclub 评论0 收藏0
  • ES6深入浅出 Classes

    摘要:一步,一步前進一步深入浅出之。是构造函数,可在里面初始化我们想初始化的东西。类静态方法大多数情况下,类是有静态方法的。中添加类方法十分容易类方法和静态方法是同一个东西在的语法中,我们可以使用关键字修饰方法,进而得到静态方法。 一步,一步前進の一步 ES6深入浅出之Classes。翻译的同时乱加个人见解,强烈推荐阅读原作者的文章,言简意赅。es6-classes-in-depth 类语...

    array_huang 评论0 收藏0
  • 使用ES6写更好的JavaScript

    摘要:但在可以用和的地方使用它们很有好处的。它会尽可能的约束变量的作用域,有助于减少令人迷惑的命名冲突。在回调函数外面,也就是中,它指向了对象。这就意味着当引擎查找的值时,可以找到值,但却和回调函数之外的不是同一个值。 使用 ES6 写更好的 JavaScript part I:广受欢迎新特性 介绍 在ES2015规范敲定并且Node.js增添了大量的函数式子集的背景下,我们终于可以拍着胸脯...

    Dionysus_go 评论0 收藏0

发表评论

0条评论

jubincn

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<