深入理解 JavaScript 中的 class

w.rogue發表於2020-12-26

深入理解 JavaScript 中的 class(類)

學習內容:

1、 掌握ES6中建立類和物件的方式 2、 掌握ES6中類的繼承 3、 掌握ES6中靜態方法的使用和呼叫 4、 理解ES6中例項化物件的原理

一、ES5:function

1、建構函式

​ 封裝、繼承、多型是大多OOP語言都支援的特性,而JavaScript在ES5中沒有提出真正意義上的類、繼承的概念。它通過函式首字母大寫的方式告知開發者這是一個建構函式或者類(有些人會將它理解為類),但從嚴格意義上來講,它是建構函式。這對於初學者來說,看起來是非常腦溢血的。比如下方code:

// 建構函式
        function Person(name, age) {
            // 例項屬性
            this.name = name;
            this.age = age;
            // 例項方法
            this.speak = function() {
                console.log(`你好,我的名字叫${this.name},今年${this.age}歲了。`)
            }
        }
        // 例項化物件,並傳入引數
        let father = new Person("張三", 20);
        // 呼叫例項屬性
        console.log(father.name);
        // 呼叫例項方法
        father.speak();

​ 如同上方code所示,我們通過function關鍵字+首字母大寫的方式告知開發者,這不是普通的函式,而是一個特殊的函式—建構函式。如果首字母小寫則將變成一個普通函式,意義就立馬改變了。並且它們的呼叫方式也不同,普通函式就是以函式的方式呼叫函式名();,而建構函式可以以這樣的方式呼叫,同時也可以使用new關鍵字來例項化物件。

2、靜態方法

​ 在大多數OOP語言中,靜態基本上都是為了共享資料,以減少例項物件所造成的記憶體消耗。但是ES5中並沒有提出相對明確的靜態方法。但是,我們可以通過原型物件來實現靜態方法。比如下方code:

// 建構函式
        function Father(name) {
            // 例項屬性
            this.name = name;
            // 例項方法
            this.run = function() {
                console.log(`我叫${this.name},我會跑!`)
            }
        }

		// 靜態屬性
		Father.age = 20;

        // 給Father建構函式的原型中新增方法
        Father.prototype = {
            // 重新指向建構函式
            constructor: Father,
            // 原型方法
            speak: function() {
                console.log("我會說話!");
            },
            run: function() {
                console.log("我會跑!");
            }
        };
        // 例項化物件
        var father = new Father("張三");
		//呼叫靜態屬性
		consloe.log(Father.age);
        // 呼叫原型方法
        father.speak();

​ 在上述code中,我們通過給建構函式的原型以物件的賦值,並將指標重新指向建構函式的方式來給建構函式新增靜態方法,而靜態屬性我們使用建構函式名.屬性名=屬性值;的方式實現,呼叫的時候使用建構函式名.屬性名;的方式呼叫。

3、繼承

​ 在大多數OOP語言中,繼承基本上都是為了減少code冗餘,提高code的可讀性和靈活性以及可維護性而存在的。但是JavaScript在ES5中並沒有提出相對明確的繼承概念。但是在ES5中,我們可以通過原型鏈和一些方法來實現真正意義上的繼承。比如下方code:

 // 基類建構函式
        function Father(name, age) {
            // 例項屬性定義在建構函式內部
            this.name = name;
            this.age = age;
            // 判斷原型中的該屬性是否是函式型別的,也就是判斷原型中是否有該方法
            if ((typeof Father.prototype.speak) !== "function") {
                // 方法定義在原型中
                Father.prototype.speak = function() {
                    console.log(`我叫${this.name},今年${this.age}歲了`)
                }
            }
        }

        // 派生類建構函式
        function Son(name, age, sex) {
            // 呼叫基類的建構函式,相當於把基類中的屬性新增到未來的派生來例項物件中
            // 將Father基類中的this指向改變為son的例項,並將基類所需要的引數傳遞
            Father.call(this, name, age);
            // 例項物件屬性
            this.sex = sex;
        }

        // 修改派生類的原型,這樣就可以繼承基類原型中的方法
        Son.prototype = new Father();
        // 例項化son,並將基類所需要的引數一併傳遞
        var son = new Son("小明", 20, "男");
        console.log(son.name);
        console.log(son.age)
        son.speak();

​ 在上述code中,我們可以看出派生類通過Son.prototype = new Father();更改派生類原型為基類例項的方式來繼承基類中的屬性和方法,而通過Father.call(this, name, age);修改this指向來呼叫基類的例項屬性和方法,將其變成派生類例項的屬性和方法,假如例項物件中有與原型同名的屬性和方法則預設替換。在ES5中它是以這樣的原理來實現繼承的。

​ 但是在實際開發中,以這樣的方式實現靜態、繼承確實是有些麻煩,於是乎JavaScript在ES6中正式提出了class(類)、constructor(建構函式)、extends(繼承)、static(靜態)的概念。

二、ES6:class

​ 2015年6月JavaScript正式釋出ES6版本,並且最重要的特點之一就是,引入了class的概念,使全球js開發者收益,使得js開發者終於告別了,直接使用原型物件模仿物件導向中的類和類繼承的時代。

​ 但是js中並沒有一個真正的class原始型別,class僅僅只是對原型物件運用的語法糖。所以,只有理解如何使用原型物件實現類和類繼承,才能真正地用好class。那麼先上code讓大家體驗一下:

class Person {
            // 建構函式
            constructor(age) {
                this.age = age;
            };
            // 靜態方法
            static speak = function(age) {
                console.log(`我的名字叫${Person.uName},今年${age}歲了`)
            };
        }
        // 靜態屬性
        Person.uName = "你好";
        // 例項化物件
        var father = new Person(20);
        // 呼叫屬性和方法
        console.log(Person.uName);
        Person.speak(father.age);
        // 試圖修改靜態屬性,但是不可以
        Person.name = "小明";
        console.log(Person.uName);

​ 我們看到出現了一個個新的關鍵字,關鍵字class用於宣告一個類,關鍵字constructor用於宣告建構函式,關鍵字static用於宣告靜態方法,而靜態屬性則於ES5中一樣,同樣只能寫在類的外邊。並且靜態屬性和方法不能被例項物件所呼叫,只能被類所呼叫。

​ ES6的語法很簡單,並且也很容易理解。但是在例項化的背後,究竟是什麼在起作用呢?

三、class例項化背後的原理

​ 使用class 的語法,讓開發者告別了使用prototype原型物件模仿物件導向的時代。但是,class並不是ES6引入的全新概念,它的原理依舊是原型物件和原型鏈。

​ 我們前邊不是學習了測試資料型別的知識嘛,那麼我們是不是就可以使用typeof來檢測類的型別,請看下方code:

// 宣告一個類 
class Person {
            // 建構函式
            constructor(age) {
                this.age = age;
            };
            // 靜態方法
            static speak = function(age) {
                console.log(`我的名字叫${Person.uName},今年${age}歲了`)
            };
        }
        // 靜態屬性
        Person.uName = "你好";
        // 例項化物件
        var father = new Person(20);
 		// 測試Person的型別
        console.log(typeof Person);

在這裡插入圖片描述

​ 通過typeof資料型別測試,我們驚奇的發現,class並非什麼全新的資料型別,它實際上只是function函式型別。

​ 為了能夠更加直觀的瞭解Person類,我們可以將它的例項物件在控制檯上輸出,結果如下:

在這裡插入圖片描述

​ 我們發現Person例項物件的屬性並不多,除去內建屬性外,大部分屬性根據名字都能明白它的作用。其中需要我們關注的兩個屬性是prototype原型物件__proto__例項物件的原型。相信大家肯定會問:不是ES5中的原型和原型鏈嘛?沒錯,這就是原型物件。也就是說使用class建立物件的方式就是隱式的使用原型物件和原型鏈。

分析:

1.	ES6中`宣告類和建構函式`其實就等於ES5中使用`function`宣告的建構函式
2.	而靜態方法和靜態屬性其實就是在原型物件中設定的屬性和方法
3.	在例項化物件的時候,瀏覽器自動建立了一個物件(原型物件),並且將該物件所屬類的建構函式與原型物件繫結,或者說原型物件記錄了建構函式的引用並指向了建構函式,而建構函式的`prototype`屬性指向了原型物件
4.	當通過例項屬性呼叫靜態屬性和方法的時候,例項屬性的`__proto__`屬性又指向了原型物件
5.	那麼這無疑是ES5中建構函式、例項物件與原型物件的三角關係

在這裡插入圖片描述

​ **溫馨提示:**如果有看不懂的朋友,請看博主上兩篇博文,詳細講解了原型物件和基與原型物件的繼承。

四、繼承

​ 在傳統物件導向中(ES5物件導向),類是可以繼承類的,這樣派生類就可以使用基類中的屬性和方法,來達到code複用的目的。

​ 當然,ES6中也可以實現繼承。ES6中明確提出了類的繼承語法,即使用extends關鍵字,並使用super()方法呼叫基類的建構函式。其實super()類似於call()中的借調繼承。請看下方code:

     // 基類
        class Anima {
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
            say() {
                console.log(`我的名字叫${this.name},今年${this.age}歲了`);
            }
        }
        // 派生類繼承基類
        class Dog extends Anima {
            constructor(name, age, sex) {
                // 呼叫基類的構造
                super(name, age)

                // 例項成員
                this.sex = sex;
                // 靜態成員
                Dog.color = "紅色";
                // 靜態方法
                Dog.run = function() {
                    console.log("我想睡覺");
                }
            }

            // 方法重寫
            say() {
                console.log(`我的名字叫${this.name},今年${this.age}歲了,顏色是${Dog.color}`);
            }
        }
        var dog = new Dog("大黃", 18, "男");
        dog.nm = "哈哈"
        dog.say()
        Dog.run();
        console.log(dog);
        console.log(Dog);

​ 實際上ES6中的繼承與ES5中的繼承原理是一樣的,其本質都是基與原型物件和原型鏈的繼承。

五、物件常用方法

  1. Object.key(物件):獲取當前物件中的屬性名,並返回一個包含所有屬性名的陣列。
  2. Object.defineProperty():該方法用於設定、修改、新增物件中的屬性
Object.defineProperty(物件,修改或新增的屬性名,{
		value:修改或新增的屬性的值,
		writable:true/false,//如果值為false 不允許修改這個屬性值
		enumerable: false,//enumerable 如果值為false 則不允許遍歷
        configurable: false  //configurable 如果為false 則不允許刪除這個屬性 屬性是否可以被刪除或是否可以再次修改特性
})	

體驗:

 		// 基類
        class Anima {
            constructor(name, age) {
                this.name = name;
                this.age = age;
            }
            say() {
                console.log(`我的名字叫${this.name},今年${this.age}歲了`);
            }
        }

        // 派生類繼承基類
        class Dog extends Anima {
            constructor(name, age, sex) {
                // 呼叫基類的構造
                super(name, age);
                // 例項成員
                this.sex = sex;
                // 靜態成員
                Dog.color = "紅色";
                // 靜態方法
                Dog.run = function() {
                    console.log("我想睡覺");
                }
            }

            // 方法重寫
            say() {
                console.log(`我的名字叫${this.name},今年${this.age}歲了,顏色是${Dog.color}`);
            }
        }
		// 例項化dog物件
        var dog = new Dog("大黃", 18, "男");
		// 設定dog物件中的sex屬性
        Object.defineProperty(dog, "sex", {
            value: "小明",
            writable: false,
            enumerable: false,
            configurable: false
        });
        // 試圖修改,但是並不能修改
        dog.sex = "女";
		// 列印的還是 小明
        console.log(dog.sex);
		// 以陣列的形式獲取物件中所有物件的屬性
        // 注意:由於我們上邊設定sex屬性不可遍歷,所以無法通過key()方法獲取到sex屬性,因為key()方法的原理是通過遍歷查詢的
        console.log(Object.keys(dog));

六、總結

​ 雖然ES5中,class(類)的繼承和靜態方法要比ES5中的語法簡單,好理解。但是js中並沒有一個真正的class原始型別,class僅僅只是對原型物件運用的語法糖。

​ 簡單來說,js中不管是ES5還是ES6中的繼承和靜態方法的實現都是基與原型物件和原型鏈實現的。所以,只有理解ES5中如何使用原型物件實現類和類繼承,才能真正地用好ES6中的class。希望大家不要因為ES6的語法簡單而忽略ES5中的知識。

不管是ES6也好,ES7也罷,基本上都是基與ES5來封裝實現的,所以希望各位博友能夠先學好ES5。同時也希望大家能夠廣集意見,如果博主有說的不對的地方還請給位博友糾正。當然如果還有不懂的地方,博主也會給大家詳細解釋。最後感謝各位博友的支援,以及感謝CSDN能夠提高這樣好的一個平臺,謝謝!

相關文章