FreeTube —— 一款開源桌面 YouTube 播放器,基於 Electron 實現,同時支 Windows(10 及更高版本)、Mac(macOS 10.15 及更高版本)和 Linux。 ...
基於原型編程
在面向對象的編程語言中,類和對象的關係是鑄模和鑄件的關係,對象總是從類創建而來,比如Java中,必須先創建類再基於類實例化對象。
而在基於原型編程的思想中,類並不是必須的,對象都是通過克隆另外一個對象而來,這個被克隆的對象就是原型對象。
基於原型編程的語言通常遵循下麵的規則:
- 所有的數據都是對象
- 要得到一個對象,不是通過實例化類,而是找到一個對象作為原型並克隆它
- 無法自己直接創建一個對象,對象都是克隆產生,必須有一個根對象,然後克隆它才可以創建對象
- 對象會記住它的原型
- 如果對象無法響應某個請求,它會把這個請求委托給自己的原型
函數和對象
JavaScript是基於原型的語言,但JavaScript並非嚴格遵循原型編程的第一條規則,其中基本類型undefined、number、string、boolean就不是對象,其模仿Java,分為了基本類型和對象類型,當然和Java一樣,基本類型可以使用包裝的形式變為對象。
這裡先探討一個問題,有助於後面理解原型和原型鏈,那就是“所有的數據都是對象,函數也是對象“,雖然函數是對象,但是函數擁有很多普通對象不具備的特性:
- 可執行性:函數可以被調用執行
- 閉包:函數可以創建閉包
- 構造函數:函數可以作為構造函數,去創建對象實例,理論上,除了
Object.create
創建的對象,都是通過構造函數創建的對象。 - 原型對象屬性:除了箭頭函數,每個函數都有一個
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原型鏈
瞭解完原型之後,再來看原型鏈就非常簡單了。
- 對象可以調用其構造函數的
prototype
原型對象的屬性和方法 - 原型對象也是對象,同樣也可以調用其構造函數的prototype原型對象的屬性和方法
- 一層一層的形成一條鏈路就是原型鏈
如圖所示:
解讀:
- 在瀏覽器中,通常使用對象的
__proto__
即可找到對象的原型對象,每個對象都有__proto__
屬性(還是要去除Object.create創建的),用於指向它的原型對象。 - 前面基於原型編程有一個規則是,必須有一個根對象,JavaScript中根對象是:
Object.prototype
,其是一個空對象。
原型鏈經典圖片
最後結合上面的內容,來看看下麵的經典原型鏈圖:
從上圖可以看出左邊是實例對象,中間是構造函數,右邊是原型對象,圖中還包含了一些其它內容:
函數是一個函數,同時也是一個對象:
構造函數作為一個函數時,擁有原型對象屬性 prototype
同時函數也是一個對象,函數都是由構造函數 Function
構造出來的,包括 Function
函數本身,可以看上圖中 function Foo()
、function Object()
、function Function()
的 __proto__
都是指向 Funtion.prototype
,這是函數比較特殊的一點。
這個地方有點繞,再理一下,函數的 prototype
屬性是這個函數自己的屬性,而函數的 __proto__
指向的是其構造函數的原型對象,可以理解為父級的屬性,兩者是不同的。
總結
- JavaScript是基於原型編程,創建對象是通過克隆對象的形式,不是通過類創建。
- 函數都擁有
prototype
原型屬性,實例化對象的__proto__
屬性指向這個原型屬性,對象可以直接調用原型對象的方法和屬性,不用寫__proto__
再調用,兩者效果一致。 - 對象的
__proto__
指向構造函數的prototype
,構造函數的prototype
同樣是對象,其__proto__
指向上一層原型對象,直到Object.prototype
,形成原型鏈。