1. 程式人生 > 實用技巧 >JavaScript calss語法糖

JavaScript calss語法糖

JavaScript calss語法糖

基礎知識

  嚴格意義上來講,在Js中是沒有類這一概念的。

  我們可以運用前面章節提到的建構函式來模擬出類這一概念,並且可以通過原型物件的繼承來完美的實現例項物件方法複用。

  但是這樣十分的麻煩,我們需要將例項物件需要用到的公共方法來存放到建構函式的原型物件中,而使用class語法糖整個過程就變得非常簡單。

宣告定義


  使用class來定義類,類中定義的函式稱為方法,不需要用關鍵字function也不需要用逗號進行分割。

<script>"use strict";
​
    class User {
​
        f1() {
            console.log(
"運行了f1..."); } // 不需要逗號分隔 f2() { console.log("運行了f2..."); } ​ } ​ let u1 = new User(); ​ u1.f1(); // 方法呼叫 u1.f2(); // 方法呼叫 </script>

建構函式


  使用 constructor 建構函式傳遞引數,該函式會在new時自動執行。

<script>"use strict";
​
    class User {
​
        constructor(name, age, gender) {
            
this.name = name; this.age = age; this.gender = gender; } ​ show(){ console.log(`姓名:${this.name},年齡:${this.age},性別:${this.gender}`); } ​ } ​ let u1 = new User("雲崖",18,"男"); ​ u1.show(); // 執行方法 </script>

  建構函式不是必須定義的,當沒有定義建構函式時。

  它會自動去查詢原型鏈,相當於如下程式碼所示。

constructor(...args) {
  super(...args);
}

原理分析


  不管是使用建構函式還是class語法糖,其原理都是一樣的。

  但是建構函式中的方法應該存放進原型物件,這一步需要我們手動去做,使用class語法糖的結構後則會自動將方法放入原型物件。

<script>"use strict";
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log("U1 show");
        }
    }
    
    console.log(typeof U1);  // function  class只是語法糖,內部還是函式。
</script>

<script>"use strict";
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log("U1 show");
        }
    }
​
    let u1 = new U1("class語法糖");
​
    console.dir(u1);
​
    // ============  兩種操作一模一樣 class自動將方法寫入原型物件中
function U2(name) {
        this.name = name;
    }
​
    U2.prototype.show = function () {
        console.log("U2 show");
    }
​
    let u2 = new U2("建構函式");
​
    console.dir(u2);
​
</script>

遍歷差異


  雖說class定義的類歸根結底還是函式,但是與我們手動建立建構函式還是有一些優化措施的。

  class 中定義的方法不能被遍歷出來,而建構函式原型物件中的方法卻可以被遍歷出來。

<script>"use strict";
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log("U1 show");
        }
    }
​
    let u1 = new U1("class語法糖");
​
    for (let key in u1) {
        console.log(key);  // name  Ps:show不會被遍歷出來
    }
​
​
    // ============  兩種操作一模一樣 class自動將方法寫入原型物件中
function U2(name) {
        this.name = name;
    }
​
    U2.prototype.show = function () {
        console.log("U2 show");
    }
​
    let u2 = new U2("建構函式");
​
    for (let key in u2) {
        console.log(key);  // name  show  
    }
​
​
</script>

嚴格模式


  class中, 預設使用strict 嚴格模式執行

<script>// "use strict";  取消嚴格模式
​
    class U1 {
        constructor(name) {
            this.name = name;
        }
        show() {
​
            (function () {
                console.log(this);  // class中用嚴格模式執行程式碼,所以this指向為undefined
            }())
​
        }
    }
​
    let u1 = new U1("class語法糖");
​
    u1.show();
​
​
    // ============  兩種操作一模一樣 class自動將方法寫入原型物件中
function U2(name) {
        this.name = name;
    }
​
    U2.prototype.show = function () {
​
        (function () {
            console.log(this);  // 建構函式中方法中的函式this非嚴格模式下指向為window
        }())
​
    }
​
    let u2 = new U2("建構函式");
​
    u2.show();
​
</script>

靜態訪問

靜態屬性


  靜態屬性即為類自己單獨設定屬性,而不是為生成的物件設定,請使用static進行宣告。

<script>"use strict";
​
    class User {
​
        static username = "使用者類";  // 如果不加 static,將會變成例項屬性
​
        constructor(username) {
​
            this.username = username;
        }
​
    }
​
    let u1 = new User("雲崖");
​
​
    console.log(u1.username);  // 雲崖
    console.log(User.username);  // 使用者類
</script>

  實現原理也非常簡單。

<script>

    "use strict";


    function User(username) {

    
        this.username = username;

    }

    User.username = "使用者類";

    let u1 = new User("雲崖");


    console.log(u1.username);  // 雲崖
    console.log(User.username);  // 使用者類


</script>

靜態方法


  靜態方法即為類自己單獨設定方法,而不是為生成的物件設定,請使用static進行宣告。

<script>

    "use strict";

    class User {

        static show(){
            console.log("類的方法...");
        }

    }

    let u1 = new User();

    User.show(); 

    u1.show();  // 丟擲異常

</script>

  實現原理也非常簡單。

<script>

    "use strict";

    function User() { }  // 建構函式

    User.show = function () { console.log( "類的方法..."); };

    let u1 = new User();

    User.show();

    u1.show();  // 丟擲異常

</script>

訪問器

  使用訪問器可以對物件的屬性進行訪問控制,下面是使用訪問器對私有屬性進行管理。

語法介紹


  使用訪問器可以管控屬性,有效的防止屬性隨意修改

  訪問器就是在函式前加上 get/set修飾,操作屬性時不需要加函式的擴號,直接用函式名

<script>

    "use strict";

    class User {

        constructor(username){
            this.username = username;
        }

        get name(){  // 訪問name時返回username

            return this.username;
        }

        set name(value){  // 設定name其實是給username做設定
            this.username = value;
        }

    }

    let u1 = new User("雲崖");
  
    console.log(u1.name);  

</script>

屬性保護


  當外部嘗試修改某一屬性時,可以使用訪問器來進行驗證。

<script>

    "use strict";

    class User {

        constructor(username){
            this.username = username;
        }

        get name(){  // 訪問name時返回username

            return this.username;
        }

        set name(value){  // 設定name其實是給username做設定

            if(typeof value == String){
                this.username = value;
            }
            
            throw Error("value type error,must string");
        }

    }

    let u1 = new User("雲崖");

    u1.name = 18;  // value type error,must string

    console.log(u1.name);  

</script>

私有封裝

  私有封裝是指內部可以任意呼叫,外部只能通過訪問的接口才能進行呼叫。

public


  public 指不受保護的屬性,在類的內部與外部都可以訪問到

<script>

    "use strict";

    class User {

        constructor(username){
            this.username = username;
        }

        show(){
            return this.username; // 內部可以訪問
        }

    }

    let u1 = new User("雲崖");
  
    console.log(u1.username); // 外部也可以訪問

    console.log(u1.show());  

</script>

protected


  protected是受保護的屬性修釋,不允許外部直接操作,但可以繼承後在類內部訪問,下面將介紹protected三種封裝方法。

命名保護

  將屬性定義為以 _ 開始,來告訴使用者這是一個私有屬性,請不要在外部使用。

  外部修改私有屬性時可以使用訪問器 setter 操作

  但這只是提示,就像吸菸時煙盒上的吸菸有害健康,但還是可以抽

<script>

    "use strict";

    class User {

        constructor(username){
            this._username = username;
        }

        show(){
            return this._username; // 內部可以訪問
        }

    }

    let u1 = new User("雲崖");
  
    // console.log(u1._username); // 外部也可以訪問,但是如果你是專業人員看到 _開頭就知道不該在外部拿他。

    console.log(u1.show());  

</script>

Symbol

  由於我們的程式碼都是在一個模組中進行封裝的,所以使用Symbol()來進行私有封裝非常方便。

  除非使用者開啟原始碼找到變數名key,否則他只能通過提供的介面來拿到資料。

<script>

    "use strict";

    let key = Symbol();

    class User {

        constructor(username){
            this[key] ={username};
        }

        show(){
            return this[key].username; // 內部可以訪問
        }

    }

    let u1 = new User("雲崖");
  
    console.log(u1);  //  { Symbol(): {username: "雲崖"}} 外部拿不到。只能通過介面來拿
    console.log(u1.show());  

</script>

WeakMap

  WeakMap是一組鍵/值對的集,下面利用WeakMap型別特性定義私有屬性

<script>

    "use strict";

    let key = new WeakMap();

    class User {

        constructor(username) {
            key.set(this, {  // 以當前物件作為鍵來儲存。
                username,
            })
        }

        show() {
            return key.get(this).username; // 內部可以訪問
        }

    }

    let u1 = new User("雲崖");

    console.log(u1);  //  {}  乾乾淨淨,啥都拿不到。
    console.log(u1.show());  // 雲崖

</script>

private


  private 指絕對私有屬性,只在當前類可以訪問到,並且不允許繼承的子類使用

  為屬性或方法名前加 # 為宣告為私有屬性

  私有屬性只能在宣告的類中使用

<script>

    "use strict";



    class User {

        #username;  // 對於例項的私有屬性來說,必須先定義一下。方法則不用。

        constructor(username) {
            this.#check(username);
            this.#username = username;
        }

        show() {
            return this.#username; // 內部可以訪問
        }

        #check = (username) => {  // 驗證類的私有方法。 私有方法格式必須如此。
            if (typeof username != "string") {
                throw Error("type error,value type must string.")
            }
        }

    }

    let u1 = new User("雲崖");
    
    console.log(u1);  // {#username: "雲崖", #check: ƒ}

    console.log(u1.show());   // 只能通過介面訪問

    // console.log(u1.#check);  // 外部不能訪問,丟擲異常。

    // console.log(u1.#username); // 外部不可以訪問,丟擲異常 

</script>

繼承特性

  class內部也是採用原型繼承,這與建構函式如出一轍。

  如果你不瞭解原型繼承,那麼可以看一下我前面的一篇文章,解釋的非常清楚了。

  這裡就是介紹一些class語法糖如何使用繼承。

extends


  以下示例將展示使用extends語法實現原型繼承。

  需要注意的是,只要繼承的子類中有建構函式,就一定要使用super方法。

  沒使用super引發異常

<script>

    "use strict";

    class A {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log(`${this.name}執行A類的show...`);
        }
    }

    class B extends A {  // 繼承A的原型物件
        constructor(name) {

            this.name = name; 

            // Uncaught ReferenceError: 
            // Must call super constructor in derived 
            // class before accessing 'this' or returning from derived constructor
            
            // 意思是必須用super

        }
    }

    let b1 = new B("例項b1");

    b1.show();

</script>

  正確示範

<script>

    "use strict";

    class A {
        constructor(name) {
            this.name = name;
        }
        show() {
            console.log(`${this.name}執行A類的show...`);
        }
    }

    class B extends A {  // 繼承A的原型物件
        constructor(name) {

            super(name);  // 必須使用super,這裡用父類的建構函式進行構造。

        }
    }

    let b1 = new B("例項b1");

    b1.show();  // 例項b1執行A類的show...

</script>

  原理如下

<script>

    "use strict";


    function A(name) {
        this.name = name;
    }

    A.prototype = {

        constructor: A,

        show() {
            console.log(`${this.name}執行A類的show...`);
        },
    };

    function B(name){
        A.call(this,name);
    }

    Object.setPrototypeOf(B.prototype,A.prototype); // B原型物件繼承與A原型物件

    let b1 = new B("例項b1");

    b1.show();  // 例項b1執行A類的show...

</script>

super


  表示從當前原型中執行方法。

  super 一直指向當前物件

  在建構函式中,super一定要放在this宣告的前面

  super 只能在類或物件的方法中使用,而不能在函式中使用,換而言之,不要使用function來表示這是一個函式!

  正常呼叫建構函式示範

<script>

    "use strict";

    class User {

        constructor(name) {
            this.name = name;
        }
        
        show() {
            console.log(`姓名:${this.name},年齡:${this.age},性別:${this.gender}`); // this指向始終為當前物件
        }
    }

    class Admin extends User {  // 繼承User的原型物件

        constructor(name,age,gender) {

            super(name);  // 使用父類的建構函式進行構造name,其他引數由自身構造。必須放在this上面

            this.age = age; 
            this.gender = gender; 

        }

        func() {
            super.show(); // 使用super呼叫的同時會將當前物件this進行傳遞。
        }
    }

    let a1 = new Admin("雲崖",18,"男");

    a1.func();  // 姓名:雲崖,年齡:18,性別:男

</script>

  super呼叫其他方法的示範

<script>

    "use strict";

    let obj_1 = {
        show(){
            console.log("執行了show..."); 
        },
    };

    let obj_2 = {

        __proto__:obj_1,

        run(){
        
            super.show();  // 正常執行
           
        },

    };

    obj_2.run(); // 執行了show...

</script>

  錯誤示範,不能用function進行宣告

<script>

    "use strict";

    let obj_1 = {
        show() {
            console.log("執行了show...");
        },
    };

    let obj_2 = {

        __proto__: obj_1,

        run = function () {

            super.show();  // 'super' keyword unexpected here
        
        },

    };

    obj_2.run(); // 異常,super不能在function中執行

</script>

靜態繼承


  類自身的屬性以及方法都能被繼承下來。

<script>

    "use strict";

    class A {

        static description = "類A";

        static show(){
            return ("類A的方法");
        }

    }

    class B extends A {  // 繼承A的原型物件

    }

    console.log(B.description);  // 類A
    console.log(B.show());  // 類A的方法

</script>

方法覆寫


  當父類原型物件中有一個方法與子類原型物件中方法同名,子類的例項物件將呼叫子類的原型物件中的方法。

  這是由原型鏈屬性順序查詢引起的。

<script>

    "use strict";

    class A {

        show() {
            console.log("A-->show");
        }

    }

    class B extends A {  // 繼承A的原型物件

        show() {
            console.log("B-->show");
        }

    }

    let b1 = new B();

    b1.show()  // B-->show

</script>

MixIn機制


  由於Js不支援多繼承,所以想新增功能必須在某一個原型物件上不斷的增加功能,這勢必會讓其本來的原型顯得混亂不堪。

  這種時候就可以使用Mixin機制來實現。

  注意:Minin類應該當做工具箱來使用,而不應該作為其他類的父類被繼承下來。

<script>

    "use strict";

    class Vehicle {
        // 交通工具類
        constructor(name) {
            this.name = name;
        }

        whistle() {
            console.log(`${this.name}在鳴笛`);  // 公用方法放父類中
        }
    }


    class Aircraft extends Vehicle {
        constructor(name) {
            super(name);
        }
    }

    class Car extends Vehicle {
        constructor(name) {
            super(name);
        }
    }


    let Flyable_Mixin = {
        // 飛行器的功能
        fly() {
            console.log(`${this.name}在飛`);
        },
        outer() {
            console.log("其他功能...");
        },
    };

    Object.assign(Aircraft.prototype, Flyable_Mixin); //給飛機新增上飛機Mixin的功能


    let Car_Mixin = {
        // 汽車的功能

        reversing() {
            console.log(`${this.name}正在倒車入庫`);
        },
        outer() {
            console.log("其他功能...");
        },
    };


    Object.assign(Car.prototype, Car_Mixin); //給汽車新增汽車Mixin的功能
    

    let c1 = new Car("法拉利");
    let a1 = new Aircraft("波音747");


    c1.whistle();  //  法拉利在鳴笛
    c1.reversing();  // 法拉利正在倒車入庫

    a1.whistle();  // 波音747在鳴笛
    a1.fly();  // 波音747在飛

</script>
程式碼示例

原型檢測

檢測方法


  使用instanceof檢測建構函式的 prototype 屬性是否出現在某個例項物件的原型鏈上

  使用isPrototypeOf檢測一個物件是否是另一個物件的原型鏈中

<script>

    "use strict";

    class User {

    } 


    let u1 = new User();

    console.log(u1 instanceof User);  // true  u1的原型鏈中包含User的原型物件嗎?

    console.log(User.prototype.isPrototypeOf(u1)); // true User的原型物件在u1的原型鏈中嗎?

</script>

instanceof原理


  遞迴不斷向上檢測原型物件。

    function checkPrototype(obj, constructor) {
        if (!obj.__proto__) return false;
        if (obj.__proto__ == constructor.prototype) return true;
        return checkPrototype(obj.__proto__, constructor);
    }