由一篇ES6繼承文章引起對於super關鍵字的思考

問題引入

最近一直在看原型繼承相關的東西,翻到這麼一篇文章: 從ES6中的extends講js原型鏈與繼承javascript

文中有一個點讓我很感興趣,箭頭函數在繼承過程當中沒法經過super關鍵字獲取,這是爲何呢?html

前置知識

MDN上關於super的介紹

The super keyword is used to access and call functions on an object's parent - in MDN
大概有這麼幾個關鍵點:java

  1. 子類中存在constructor方法的時候,須要調用super方法,而且須要在使用this關鍵字以前調用
  2. super關鍵字能夠用來調用父對象上的方法
  3. 可使用super來調用父對象上的靜態方法
  4. 不可使用delete來刪除super上的屬性
  5. 不能夠複寫super對象上的只讀屬性

子類中是否必須主動調用super方法?

個人見解是不須要。
網上有些文章(好比這篇)寫道:es6

由於若不執行super,則this沒法初始化。

個人我的理解是,this是指代執行上下文環境的,不存在沒法初始化的狀況。更準確的說法是這樣:若是不使用super方法,那麼父類中的屬性值沒法進行初始化,若是這個時候子類經過this字段來訪問了父類中的屬性值,那麼只能獲得一個undefined。至於爲何這麼寫編譯的時候會報錯?個人理解是,這應該是一種語法錯誤,並且是一種規範要求,ES6語法的規範要求,這種要求並非說會影響到代碼的實際執行。舉個栗子:typescript

// typescript中一段簡單的繼承代碼實現
class Parent {
    name = 'parent';
    func = function() {
        console.log('func in parent called.');
    }
}

class Child extends Parent {
    age = 3;
    func = function() {
        console.log('age is: ', this.age);    // 使用了this,不會報錯
    }
}

這段代碼很是簡單,在子類中使用了this關鍵字,編譯時不會報錯,也能夠正常執行。而後咱們進行一點修改,在子類中引入constructor方法segmentfault

class Child extends Parent {
    age = 3;
    // error TS2377: Constructors for derived classes must contain a 'super' call.
    constructor() {
        
    }
    func = function() {
        console.log('age is: ', this.age);
    }
}

能夠看到,編譯階段已經開始報錯了。在typescript的語法中,子類的constructor方法中不但須要調用super方法,並且必須在第一行代碼就調用super,不然都是會報錯的。看下面這段代碼:app

class Child extends Parent {
    age = 3;
    constructor() {
        console.log('First line in constructor without super method');
        super();    // error TS2376: A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.
    }
    func = function() {
        console.log('age is: ', this.age);
    }
}

來,咱們接着改函數

class Parent {
    name = 'parent';
    func = function() {
        console.log('func in parent called.');
    }
}

class Child extends Parent {
    age = 3;
    constructor() {
        console.log('Show property of parent, name is: ', this.name);    // error TS17009: 'super' must be called before accessing 'this' in the constructor of a derived class.
        console.log('Show property of child, age is: ', this.age);        // error TS17009: 'super' must be called before accessing 'this' in the constructor of a derived class.
        super();    // error TS2376: A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.
        console.log('Show property of parent, name is: ', this.name);
        console.log('Show property of child, age is: ', this.age);
    }
    func = function() {
        console.log('age is: ', this.age);
    }
}

能夠看到,編譯期已經開始報各類錯誤了,不過這不重要,咱們這裏利用typescript的編譯器(tsc)來進行編譯,並查看編譯後的代碼內容:this

var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Parent = (function () {
    function Parent() {
        this.name = 'parent';
        this.func = function () {
            console.log('func in parent called.');
        };
    }
    return Parent;
}());
var Child = (function (_super) {
    __extends(Child, _super);
    function Child() {
        var _this = this;
        _this.age = 3;
        _this.func = function () {
            console.log('age is: ', this.age);
        };
        console.log('Show property of parent, name is: ', _this.name); // 輸出undefined,由於此時子類的實例上尚未繼承到父類的屬性值
        console.log('Show property of child, age is: ', _this.age); // 輸出3,子類實例本身的屬性值能夠訪問
        _this = _super.call(this) || this; // 構造函數式的繼承實現,這一步就是講父類的屬性值設置到子類實例上
        console.log('Show property of parent, name is: ', _this.name); // 輸出parent,此時子類的實例上通過上一步的繼承,獲得了父類的屬性值
        console.log('Show property of child, age is: ', _this.age);  // 輸出3,子類實例本身的屬性值能夠訪問
        return _this;
    }
    return Child;
}(Parent));
//# sourceMappingURL=demo.js.map

由此能夠知道,在ES6中使用extends進行繼承操做的過程當中,prototype

  • 子類並不是必須調用super方法,除非存在constructor方法
  • 在constructor方法中應該首先調用super方法,這是語法要求,不過這不是必須的
  • 在調用super方法以前,將沒法經過this關鍵字來訪問父類的屬性(這裏就能夠解釋其餘文章中提到的 ‘若不執行super,則this沒法初始化’,更準確的說法應該是‘若不執行super,則沒法將父類的屬性值初始化到當前子類實例上’)

子類中使用super.prop和super[expr]的方式是如何訪問父類的屬性和方法?

咱們直接來看代碼吧,關鍵點都註釋了的

class Parent {
    public name = 'parent';
    public static staticName = 'staticParent';
    public static staticFunc() {
        console.log('staticFunc called in parent.');
    }

    public arrowFunc = () => {
        console.log('arrowFunc called in parent.');
    }

    public normalFunc() {
        console.log('normalFunc called in parent.')
    }
}

class Child extends Parent {
    public static staticFunc() {
        super.staticFunc();
        console.log('staticFunc called in Child.');
    }

    arrowFunc = () => {
        super.arrowFunc();
        console.log('arrowFunc called in Child.');
    }

    normalFunc() {
        super.normalFunc();
        console.log('normalFunc called in Child.')
    }

    getName() {
        console.log('parent name is: ', super.name);
        console.log('parent staticName is: ', super.staticName);
        console.log('child name is: ', this.name);
    }
}

/** 編譯後的代碼 **/
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Parent = (function () {
    function Parent() {
        this.name = 'parent';
        this.arrowFunc = function () {
            console.log('arrowFunc called in parent.');
        };
    }
    // 編譯後的靜態方法能夠存在於Parent類的內部
    Parent.staticFunc = function () {
        console.log('staticFunc called in parent.');
    };
    Parent.prototype.normalFunc = function () {
        console.log('normalFunc called in parent.');
    };
    return Parent;
}());
Parent.staticName = 'staticParent'; // 編譯後的靜態屬性依然存在於Parent類外
var Child = (function (_super) {
    __extends(Child, _super);
    function Child() {
        var _this = _super !== null && _super.apply(this, arguments) || this;
        _this.arrowFunc = function () { // 子類實例調用arrowFunc的時候會報錯,由於_super.prototype上是不存在arrowFunc方法的
            _super.prototype.arrowFunc.call(_this); // Uncaught TypeError: Cannot read property 'call' of undefined
            console.log('arrowFunc called in Child.');
        };
        return _this;
    }
    Child.staticFunc = function () {
        _super.staticFunc.call(this);   // super能夠正常訪問父類的靜態方法
        console.log('staticFunc called in Child.');
    };
    Child.prototype.normalFunc = function () {
        _super.prototype.normalFunc.call(this);
        console.log('normalFunc called in Child.');
    };
    Child.prototype.getName = function () {
        console.log('parent name is: ', _super.prototype.name); // 輸出undefined, 父類原型(_super.prototype)上不存在name屬性
        console.log('parent staticName is: ', _super.prototype.staticName); // 輸出undefined,super沒法正常訪問父類的靜態屬性
        console.log('child name is: ', this.name);  // 輸出parent,這是子類實例上的屬性,繼承自父類
    };
    return Child;
}(Parent));
//# sourceMappingURL=demo.js.map

這裏再順嘴提一句,關於靜態屬性和靜態方法的區別。爲何在子類中經過super關鍵字來獲取父類的靜態方法通過編譯後是_super.staticFunc,而獲取靜態屬性依然是_super.prototype.staticName,從原型上獲取致使獲取失敗呢?這個問題目前我尚未找到答案,但願有知道的小夥伴能夠不吝指教。
不過我卻是搜到一些其餘相關內容。
Class 的靜態屬性和實例屬性

由於 ES6 明確規定,Class 內部只有靜態方法,沒有靜態屬性。

雖然這種規定從ES7開始獲得了修正,咱們目前已經能夠將靜態屬性寫在Class的內部,可是通過編譯以後能夠發現,靜態屬性依然存在於類的實現的外部。

var Parent = (function () {
    function Parent() {
        this.name = 'parent';
        this.arrowFunc = function () {
            console.log('arrowFunc called in parent.');
        };
    }
    // 編譯後的靜態方法能夠存在於Parent類的內部
    Parent.staticFunc = function () {
        console.log('staticFunc called in parent.');
    };
    Parent.prototype.normalFunc = function () {
        console.log('normalFunc called in parent.');
    };
    return Parent;
}());
Parent.staticName = 'staticParent'; // 編譯後的靜態屬性依然存在於Parent類外

回到問題自己

問:箭頭函數在繼承過程當中沒法經過super關鍵字獲取,這是爲何呢?
答:由於子類中使用super.prop和super[expr]的方式獲取的是父類原型(prototype)上的方法,靜態方法除外。

參考資料

從ES6中的extends講js原型鏈與繼承
React ES6 class constructor super()
Class 的靜態屬性和實例屬性

相關文章
相關標籤/搜索