對ES6中類class以及例項物件、原型物件、原型鏈之間關係的詳細總結

BAHG發表於2021-07-03

1. 類

  ES6 中新增加了類的概念,可以使用 class 關鍵字宣告一個類,之後用這個類來例項化物件。即類的用途:例項化物件。

// 建立一個Person類
class Person {
      
}
// 建立一個Person類的例項物件
const p1 = new Person()
console.log(p1)

列印結果如下:

注意,輸出的p1是一個例項物件,而不是類!這裡的輸出結果有 Person,是為了說明這個例項物件是由誰new出來的,藍框表示輸出的確實是一個例項物件。

思考:為什麼前面要帶一個類呢?假設還有一個Dog類,同樣new一個Dog類的例項物件p2,這時候必須通過類名來區分例項物件,否則就人畜不分了......

現在我們瞭解了類和例項物件,下面我們再來研究建構函式。

2. 建構函式

一般對例項物件都會有一些初始化操作,比如人有姓名和年齡,因此就出現了類的構造器方法constructor(),它的作用是給例項物件新增屬性,語法如下:

//步驟1 在類中定義建構函式constructor,函式名固定
class Person {
  constructor(name,age) {//定義形參
      this.name = name;//將形參賦值給this物件的對應屬性
      this.age = age;
    }
}       

//步驟2 在例項化物件的時候,傳遞實參
const p1 = new Person('bahg', 18); //這裡的實參預設傳遞給Person類中的constructor
console.log(p1.name);//bahg

思考:

1、建構函式必須寫嗎?答:不是必須的,但是對於Person這個類而言,它沒有繼承任何類,如果不寫構造方法也就沒有任何意義。下面會講到繼承類,它可以不寫建構函式,預設會呼叫父類的建構函式。

2、建構函式中的 this 是什麼?答:是例項化物件,也就是p1

一個類除了有構造方法,還有一般方法,一般方法是用來定義行為的,比如人可以吃飯睡覺敲程式碼,這裡我們定義一個speak方法:

 // 建立一個Person類
    class Person {
      constructor(name, age) {
        this.name = name
        this.age = age
      }
      speak() {
        console.log(`名字為${this.name}的人年齡為${this.age}`)
      }
    }
    // 建立一個Person類的例項物件
    const p1 = new Person('bahg', 18)
    const p2 = new Person('zzz', 21)
    console.log(p1)
    console.log(p2)
    p1.speak()
    p2.speak()

列印結果如下:

3. 原型物件 

在JavaScript中,每當定義一個函式資料型別(普通函式、類)時候,都會天生自帶一個prototype屬性,這個屬性指向函式的原型物件,並且這個屬性是一個物件資料型別的值。

讓我們用一張圖表示建構函式和例項原型之間的關係:

 

 原型物件就相當於一個公共的區域,所有同一個類的例項都可以訪問到這個原型物件,我們可以將物件中共有的內容,統一設定到原型物件中。

思考:

1、p1和p2為什麼沒有出現speak方法呢?被放到了哪裡?答:類的原型物件上,如下所示:

2、speak方法是給誰用的?答:給例項物件用的

3、speak的this指向誰?答:指向它的最後呼叫者。注意:“誰呼叫它就是誰”這種說法是不準確的,因為call、apply、bind都可以更改函式中的this指向,例如 p1.speak.call({a:1,b:2}) 此時this就是undefined。

4. 繼承

繼承是為了複用程式碼,下面我們再定義一個Student類,它繼承於Person類:

class Person {
   constructor(name, age) {
      this.name = name
      this.age = age
}
   speak() {
     console.log(`名字為${this.name}的人年齡為${this.age}`)
   }
}
   
class Student extends Person{

}
const s1 = new Student('小明', 15)
console.log(s1)

這裡我們沒有在Student類中寫構造方法,但是仍然可以列印s1,說明構造器方法不是非寫不可。

思考:我們什麼時候需要寫構造器方法呢?

答:子類有自己特有的屬性時,比如學生有年級這個屬性,此時就需要重新寫自己的構造器方法,但是注意:一旦寫了構造器,就必須呼叫 super() 方法,並且要寫在其他屬性之前!super函式的作用是呼叫父類的構造器。

class Student extends Person {
      constructor(name, age, grade) {
        super(name, age)
        this.grade = grade
      }
}
const s1 = new Student('小明', 15, '高一')
console.log(s1)

思考:此時Student的原型物件上有方法嗎?

答:除了構造器方法之外沒有其它方法,因為Student類裡面沒有寫自己的方法。

那麼問題來了,學生能夠說話嗎?答案是肯定的,通過s1.speak()可以列印出結果,那麼speak方法在哪呢?這裡就要引出原型鏈了。

5.原型鏈

在JavaScript中萬物都是物件,物件和物件之間也有關係,並不是孤立存在的。物件之間的繼承關係,在JavaScript中是通過prototype物件指向父類物件,直到指向Object物件為止,這樣就形成了一個原型指向的鏈條,專業術語稱之為原型鏈。

舉例說明:person → Person → Object ,普通人繼承人類,人類繼承物件類

當我們訪問物件的一個屬性或方法時,它會先在物件自身中尋找,如果有則直接使用,如果沒有則會去原型物件中尋找,如果找到則直接使用。如果沒有則去原型的原型中尋找,直到找到Object物件的原型,Object物件的原型沒有原型,如果在Object原型中依然沒有找到,則返回undefined。

對於例項物件s1,它在呼叫speak方法時,會先去原型物件上查詢,發現沒有這個方法,就繼續去原型鏈上查詢,找到了父類原型物件上的speak方法,如圖所示:

 從始至終都只有一個speak,沿著原型鏈一層層去找。我們還可以對speak方法進行重寫,什麼時候需要重寫呢?當子類要對父類方法進行擴充套件時,就可以重寫方法。

class Student extends Person {
    constructor(name, age, grade) {
       super(name, age)
       this.grade = grade
    }
    speak() {
      console.log(`名字為${this.name}的人年齡為${this.age},讀${this.grade}`)
    }
}
const s1 = new Student('小明', 15, '高一')
console.log(s1)

思考:此時Student原型物件上有speak方法嗎?答:有,s1列印結果如下:

按照原型鏈查詢規則,當它查詢到藍色箭頭的時候就直接呼叫speak函式了,不會再往下查詢。

假設現在學生類還有自己獨有的study方法,思考:

1、study方法放在了哪裡?供誰使用?答:放在了Student類的原型物件上,供例項使用。

2、通過Student例項呼叫study時,this指向誰?答:指向Student的例項。

以上就是對類的一個複習,並沒有把類的所有知識都進行復習,只是重新梳理了比較重要也是比較難理解的部分。總結如下:

1、類中的構造器不是必須寫的,要對例項進行一些初始化的操作時,如新增一些指定屬性時才寫。如果在Student的構造器中加一行 this.job = '程式設計師'也是可以的,引數列表不需要變,它表示Student締造的例項物件的工作都是程式設計師

2、如果A類繼承B類,且A類中寫了構造器,那麼A類構造器中super是必須要呼叫的

3、類中定義的方法都是放在了類的原型物件上,供例項使用

由於之前一直沒能理解類、例項物件、原型鏈等具體概念以及相互之間的聯絡,故寫下這篇部落格來幫助理解,如有錯誤請指正。

部落格中原型物件及原型鏈部分參考了https://www.jianshu.com/p/ddaa5179cda6這篇文章。

相關文章