ES6 class繼承與super關鍵詞深刻探索

ES6 class

在ES6版本以前,JavaScript語言並無傳統面嚮對象語言的class寫法,ES6發佈以後,Babel迅速跟進,廣大開發者也很快喜歡上ES6帶來的新的編程體驗。
固然,在這門「混亂」而又精妙的語言中,許多天天出現咱們視野中的東西卻經常被咱們忽略。
對於ES6語法,考慮到瀏覽器的兼容性問題,咱們仍是要把代碼轉換爲ES5版本運行。然而,以前的ES版本爲何能模仿ES6的諸多特性,好比class與繼承,super,static?JavaScript又作了哪些改變以應對這些新角色?本文將對class實例構造,class繼承關係,super關鍵字,static關鍵字的運行機制進行探索。
水平有限,文中如有引發困惑或錯誤之處,還望指出。chrome

class實例構造

class基本樣例

基本而言,ES6 class形式以下:編程

class Whatever{
      
 }

固然,還能夠有constructor方法。瀏覽器

class Whatever{
          constructor(){
             this.name = 'hahaha';
         } 
 }

請看ES5對應版本:babel

function Whatever{
    this.name = 'hahaha';
}

new幹了什麼

可知,constructor至關於之前在構造函數裏的行爲。而對於ES5構造函數而言,在被new調用的時候,大致上進行了下面四步:app

  1. 新建對象var _this = {};
  2. this的[[prototype]]指向構造函數的prototype,即_this.__proto_ = Constructor.prototype
  3. 改變Constructor的this到_this並執行Constructor,即Constructor.apply(_this,agrs);獲得構造好的_this對象
  4. 判斷Constructor的返回值,若返回值不爲引用類型,則返回_this,不然返回改引用對象

因此,構造函數的實例會繼承掛載在prototype上的方法,在ES6 calss中,咱們這樣寫會把方法掛載在class的prototype:異步

class Whatever{
    //...
    methodA(){
        //...
    }          
}

對應ES5寫法:函數

Whatever.prototype = function methodA(){
            //...
        }

class繼承關係

原型語言基本特色

在基於原型的語言,有如下四個特色:this

  1. 一切皆爲對象(js中除了對象還有基本類型,函數式第一等對象)
  2. 對象皆是從其餘對象複製而來(在JS對象世界中,萬物始於Object.prototype這顆蛋)
  3. 對象會記住它的原型(在JS中對象的__proto__屬性指向它的原型)
  4. 調用對象自己沒有的屬性/方法時,對象會嘗試委託它的原型

看到這,你們應該明白了,爲何掛載在Constructor.prototype的方法會被實例「繼承」!
在ES6 class中,繼承關係仍是由[[prototype]]連維持,即:編碼

Child.prototype.__proto__ === Parent.prototype;
Child.__proto__ === Parent;
childObject.__proto === Child.prototype;

當箭頭函數與class碰撞

ES6的箭頭函數,一出身便深受衆人喜好,由於它解決了使人頭疼的函數執行時動態this指向的「問題」(爲何加引號?由於有時候咱們有時確實須要動態this帶來的巨大便利)。箭頭函數中this綁定在詞法做用域,即它定義的地方:prototype

//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一下

當咱們談論繼承時,每每指兩種:

  1. 對象實例繼承自一個類(構造函數)
  2. 子類繼承父類

上文中咱們探討了第一種,如今,請把注意力轉向第二種。
考慮下方代碼:

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愈來愈少。

相關文章
相關標籤/搜索