徹底搞懂JavaScript原型和原型鏈

程序员李林發表於2024-05-23

基於原型程式設計

在物件導向的程式語言中,類和物件的關係是鑄模和鑄件的關係,物件總是從類建立而來,比如Java中,必須先建立類再基於類例項化物件。
而在基於原型程式設計的思想中,類並不是必須的,物件都是透過克隆另外一個物件而來,這個被克隆的物件就是原型物件。
基於原型程式設計的語言通常遵循下面的規則:

  • 所有的資料都是物件
  • 要得到一個物件,不是透過例項化類,而是找到一個物件作為原型並克隆它
  • 無法自己直接建立一個物件,物件都是克隆產生,必須有一個根物件,然後克隆它才可以建立物件
  • 物件會記住它的原型
  • 如果物件無法響應某個請求,它會把這個請求委託給自己的原型

函式和物件

JavaScript是基於原型的語言,但JavaScript並非嚴格遵循原型程式設計的第一條規則,其中基本型別undefined、number、string、boolean就不是物件,其模仿Java,分為了基本型別和物件型別,當然和Java一樣,基本型別可以使用包裝的形式變為物件。
這裡先探討一個問題,有助於後面理解原型和原型鏈,那就是“所有的資料都是物件,函式也是物件“,雖然函式是物件,但是函式擁有很多普通物件不具備的特性:

  1. 可執行性:函式可以被呼叫執行
  2. 閉包:函式可以建立閉包
  3. 建構函式:函式可以作為建構函式,去建立物件例項,理論上,除了 Object.create 建立的物件,都是透過建構函式建立的物件。
  4. 原型物件屬性:除了箭頭函式,每個函式都有一個 prototype 屬性,它是一個物件,用於儲存透過該建構函式建立的所有例項的共享屬性和方法。

JavaScript原型

從上面可以得知,函式都有 prototype 屬性,稱之為原型,也稱為原型物件,原型物件裡面可以存放屬性和方法,共享給例項物件使用,用於實現繼承效果。
這裡的函式指的是建構函式,即可以透過new建立物件的函式,在JavaScript中普通函式都可以是建構函式,除了箭頭函式。

箭頭函式沒有 prototype 屬性是為了保持它們的設計簡潔性,避免與原型鏈相關的複雜性和潛在問題。箭頭函式同時不能使用new例項化物件。如果你需要定義一個構造器函式,應該使用普通函式而不是箭頭函式。

舉例說明:

const arr = new Array(1, 2, 3)
arr.reverse()
arr.sort()

在上面的程式碼中 Array 就是一個建構函式,reverse和sort都是 Array.prototype 原型物件上的函式。
這裡有個問題,為何要將這些方法放在 prototype 原型屬性上,而不是放在建構函式內部呢?
我們來看下面的程式碼:

function Person() {
  this.getName = function () {
    console.log('person')
  }
}
Person.prototype.getProtoName = function () {
  console.log('proto person')
}
const person = new Person()
const person1 = new Person()

console.log(person.getName === person1.getName) // false
console.log(person.getProtoName === person1.getProtoName) // true

很明顯,雖然在例項化物件上都可以呼叫這些方法,但兩者有本質的不同,將方法放在 prototype 原型物件上,之後例項化的物件使用的都是同一個方法,類似於Java的類方法,而建構函式內的方法,則會重新建立,也就是例項物件方法。

JavaScript原型鏈

瞭解完原型之後,再來看原型鏈就非常簡單了。

  1. 物件可以呼叫其建構函式的 prototype 原型物件的屬性和方法
  2. 原型物件也是物件,同樣也可以呼叫其建構函式的prototype原型物件的屬性和方法
  3. 一層一層的形成一條鏈路就是原型鏈

如圖所示:

解讀:

  1. 在瀏覽器中,通常使用物件的 __proto__ 即可找到物件的原型物件,每個物件都有 __proto__ 屬性(還是要去除Object.create建立的),用於指向它的原型物件。
  2. 前面基於原型程式設計有一個規則是,必須有一個根物件,JavaScript中根物件是:Object.prototype,其是一個空物件。

原型鏈經典圖片

最後結合上面的內容,來看看下面的經典原型鏈圖:

從上圖可以看出左邊是例項物件,中間是建構函式,右邊是原型物件,圖中還包含了一些其它內容:
函式是一個函式,同時也是一個物件:
建構函式作為一個函式時,擁有原型物件屬性 prototype
同時函式也是一個物件,函式都是由建構函式 Function 構造出來的,包括 Function 函式本身,可以看上圖中 function Foo()function Object()function Function()__proto__ 都是指向 Funtion.prototype,這是函式比較特殊的一點。
這個地方有點繞,再理一下,函式的 prototype 屬性是這個函式自己的屬性,而函式的 __proto__ 指向的是其建構函式的原型物件,可以理解為父級的屬性,兩者是不同的。

總結

  1. JavaScript是基於原型程式設計,建立物件是透過克隆物件的形式,不是透過類建立。
  2. 函式都擁有 prototype 原型屬性,例項化物件的 __proto__ 屬性指向這個原型屬性,物件可以直接呼叫原型物件的方法和屬性,不用寫 __proto__ 再呼叫,兩者效果一致。
  3. 物件的 __proto__ 指向建構函式的 prototype,建構函式的 prototype 同樣是物件,其 __proto__ 指向上一層原型物件,直到 Object.prototype,形成原型鏈。

相關文章