原型與繼承(3) - class


Posted by TempuraEngineer on 2022-09-04

目錄


class

class是建構函式的語法糖,它將prototype的機制進行抽象化,雖然不推薦,但還是可以操作其prototype

比起建構函式,class的優點可讀性、易維護性高,缺點是效能比建構函式差一些、一些比較老的原始碼編譯器不支援

因為每次使用new來建立實例的時候都會呼叫class的constructor,也就是說constructor裡的屬性會被重新賦值,所以方法不該定義在constructor

For instance, when creating a new object/class, methods should normally be associated to the object's prototype rather than defined into the object constructor. The reason is that whenever the constructor is called, the methods would get reassigned (that is, for every object creation).


繼承

使用extends可以繼承另一個class,被繼承的class為父class(superclass),繼承的為子class(derived class)

繼承時子class的constructor裡一定要放super一定要在this之前被呼叫

super會呼叫父class的constructor,然後將父class的公有屬性綁過來

The super keyword is used to access properties on an object literal or class's [[Prototype]], or invoke a superclass's constructor.

class Person{
    constructor(name, skill){
        this.name = name;
        this.skill = skill;
    }
}

class Politician extends Person{
    constructor(name, skill, party){
        super(name, skill); // 一定要有super,用來呼叫父class constructor
        this.party = party;
    }

    saySomethingRidiculous(content){
        return `${this.name}: ${content}`;
    }
}

const clock = new Politician('時鐘', '報數', 'Dqp');
clock.name; // 時鐘
clock.party; // Dqp


super的指向

super用於呼叫父class的constructor,在子class的靜態方法內時,可存取子class的__proto__的靜態屬性、方法,但子class的實例方法內無法,因為實例的proto是一個實例,實例上面是不會有class的靜態屬性、方法的,所以要不是得到undefined就是...is not a function

It calls the parent class's constructor and binds the parent class's public fields, after which the derived class's constructor can further access and modify this.

Within a class's body, the reference of super can be either the superclass's constructor itself, or the constructor's prototype, depending on whether the execution context is instance creation or class initialization

class Person{
    constructor(name, skill){
        // this在建構函式裡沒有指向,直到用new建立物件,this才指向該物件
        this.name = name;
        this.skill = skill;

        // 透過class呼叫靜態方法
        Person.countPerson();
    }

    static countPerson(){
        // this指向Person
        this._count = this.count? this.count + 1 : 1;
    }    

    static get count(){
        // this指向Person
        return this._count? this._count : 0;
    }    
}

class Politician extends Person{
    constructor(name, skill, party){
        // 一定要有super,用來呼叫父class constructor
        super(name, skill); 
        this.party = party;

        Politician.countPartyMember(party);
    }

    static map = {};

    static countPartyMember(party){
        if(party in this.map){
            this.map[party]++;

        }else{
            this.map[party] = 1;
        }
    }

    static showPercentOfPolitician(){
        // 設置初始值為0,避免map內沒有屬性時報錯Reduce of empty array with no initial value
        const number = Object.values(this.map).reduce((acc, cur) => acc + cur, 0);

        // super指向Politician.__proto__,即Person class
        // 透過super可以取得父class的靜態方法、屬性
        return `There are ${super.count} people, and ${number} of them are politician.` 
    }

    getPercentOfPolitician(){
        // this會指向由Politician new出來的實例,所以this.map會是undefined
        console.log(this.map);
        const number = this.map? Object.values(this.map).reduce((acc, cur) => acc + cur, 0) : undefined;

        console.log(Object.getPrototypeOf(this)); // Person {constructor: ƒ, foo: ƒ}
        // Person實例上不存在靜態屬性count,所以super.count會得到undefined
        return `There are ${super.count} people, and ${number} of them are politician.` 
    }
}

const clock = new Politician('時鐘', '報數', 'Dqp');
const koreaFish = new Politician('韓國魚', '發大財', 'Knt');
const alex = new Person('Alex', 'programming');

Politician.showPercentOfPolitician(); // There are 3 people, and 2 of them are politician.
clock.getPercentOfPolitician(); // There are undefined people, and undefined of them are politician.

另外需要注意的是super的指向根據在哪個class被呼叫決定,而非使用super的方法

Note that the reference of super is determined by the class or object literal super was declared in, not the object the method is called on.

所以重新綁定方法不能改變super的指向(雖然會改變this的指向)

Therefore, unbinding or re-binding a method doesn't change the reference of super in it (although they do change the reference of this).

class Person{
    constructor(name, skill){
        // this在建構函式裡沒有指向,直到用new建立物件,this才指向該物件
        this.name = name;
        this.skill = skill;

        // 透過class呼叫靜態方法
        Person.countPerson();
    }

    static countPerson(){
        // this指向Person
        this._count = this.count? this.count + 1 : 1;
    }    

    static get count(){
        console.log('this is count from Person');
        // this指向Person
        return this._count? this._count : 0;
    }    
}

class Human{
    constructor(name, skinColor){
        this.name = name;
        this.skinColor = skinColor;
    }

    static map = {
        unknown:111,
    };

    static get count(){
        // this指向Human
        return 87;
    }       
}

class Politician extends Person{
    constructor(name, skill, party){
        // 一定要有super,用來呼叫父class constructor
        super(name, skill); 
        this.party = party;

        Politician.countPartyMember(party);
    }

    static map = {};

    static countPartyMember(party){
        if(party in this.map){
            this.map[party]++;

        }else{
            this.map[party] = 1;
        }
    }

    static showPercentOfPolitician(){
        // 設置初始值為0,避免map內沒有屬性時報錯Reduce of empty array with no initial value
        const number = Object.values(this.map).reduce((acc, cur) => acc + cur, 0);

        // super指向Politician.__proto__,即Person class
        // 透過super可以取得父class的靜態方法、屬性
        return `There are ${super.count} people, and ${number} of them are politician.` 
    }
}

const clock = new Politician('時鐘', '報數', 'Dqp');
const koreaFish = new Politician('韓國魚', '發大財', 'Knt');
const alex = new Person('Alex', 'programming');

Politician.showPercentOfPolitician(); // There are 3 people, and 2 of them are politician.

// this指向Politician
// super指向Person

const rebind = Politician.showPercentOfPolitician.bind(Human); 
rebind(); // There are 0 people, and 111 of them are politician.

// this指向被改成Human,所以會得到Human.map的{unknown:111}
// super指向仍是Person,所以不會得到Human.count的87


私有 & getter & setter

class的欄位預設是公開的,可以直接存取。在變數名稱前加上#能使其變私有,私有屬性常以_開頭只是慣例,實際上_並不能使方法或屬性變成私有

一般來說私有屬性,才需要getter和setter來存取、設定

class Person{
    #age = 0;

    constructor(name, birthDay){
        this.name = name;
        this.birthDay = birthDay;

        this.calculateAge();
    }    

    calculateAge(){
        this.#age = Math.floor((new Date() - new Date(this.birthDay)) / 365 / 24 / 60 / 60 / 1000);
    }

    get userAge(){ 
        // 私有屬性不能只接存取,所以透過getter return私有屬性,
        // 實例才能透過getter取得私有屬性,這樣也可以避免私有屬性從實例被改變
        return this.#age;
    }
}

const alex = new Person('Alex', '1989-08-09');
alex.userAge; // 33
alex.#age; // Private field '#age' must be declared in an enclosing class

alex.userAge = 50;
alex.userAge; // 33


靜態屬性、方法

靜態屬性、方法只能透過class、其子class取用,實例無法取用

靜態方法常作為工具函式(utility functions)使用,靜態屬性則適合用於快取、不變的設定、不需要暴露給實例的屬性

Static methods are often utility functions, such as functions to create or clone objects, whereas static properties are useful for caches, fixed-configuration, or any other data you don't need to be replicated across instances.

靜態方法this指向class

class Person{
    constructor(name, skill){
        // this在建構函式裡沒有指向,直到用new建立物件,this才指向該物件
        this.name = name;
        this.skill = skill;

        // 透過class呼叫靜態方法
        Person.countPerson();
    }

    static countPerson(){
        // this指向Person
        this._count = this.count? this.count + 1 : 1;
    }    

    static get count(){
        // this指向Person
        return this._count;
    }    
}

class Politician extends Person{
    constructor(name, skill, party){
        // 一定要有super,用來呼叫父class constructor
        super(name, skill); 
        this.party = party;

        Politician.countPartyMember(party);
    }

    static map = {};

    static countPartyMember(party){
        if(party in this.map){
            this.map[party]++;

        }else{
            this.map[party] = 1;
        }
    }

    static showPercentOfPolitician(){
        const number = Object.values(this.map).reduce((acc, cur) => acc + cur);

        // super指向Person,透過super可以取得父class的靜態方法、屬性
        return `There are ${super.count} people, and ${number} of them are politician.` 
    }

    talkingAboutParty(){
        // 透過this.constructor存取Politician,這樣就能存取Politician的靜態屬性
        const number = this.constructor.map[this.party];
        return `I came from ${this.party}, and there are ${number} member${number > 1? 's' : ''} in our party.`;
    }
}

const clock = new Politician('時鐘', '報數', 'Dqp');
const koreaFish = new Politician('韓國魚', '發大財', 'Knt');
clock.talkingAboutParty(); // I came from Dqp, and there are 1 member in our party.

const soLong = new Politician('真長', '注意!', 'Dqp');
clock.talkingAboutParty(); // I came from Dqp, and there are 2 members in our party.

Politician.map; // {Dqp:2, Knt:1}


參考資料

MDN - Class
MDN - super
MDN - private
MDN - static

ES6 的 Class 、super 的特例與繼承


#class







Related Posts

[Week4] JS 實作串接 API(三)

[Week4] JS 實作串接 API(三)

JSON-server建一支RESTful API

JSON-server建一支RESTful API

[進階 js 09] Closure & Scope Chain

[進階 js 09] Closure & Scope Chain


Comments