上一篇提到 JS 是物件原型導向,而非物件導向的語言,如果想要像物件導向那樣達成物件之間屬性的共用,就需要借助原型的幫忙,所以了解「原型」的概念,對於我們後續想要活用 JS 的物件,或是在 JS 裡面撰寫物件導向風格的程式碼的話是非常重要的。
Outline
- 物件導向:類別與物件
- 物件導向:繼承的概念
- 函式上的原型物件屬性
- 透過函式建構子產生的物件
- 總結:原型物件屬性
物件導向:類別與物件
在物件導向語言裡面,類別定義了描述某件事或某個功能的基本概念,就像一件商品或是建築物的設計圖ㄧ樣;而物件則是透過類別裡所描述的的概念實現出來的東西,對比於建築設計圖,就是建築物:
- 類別 ←→ 建築設計圖
- 物件 ←→ 建築物
當然上面的比喻只能說是非常非常粗淺的描述,完整的物件導向概念是非常博大精深的。這邊是想讓各位讀者了解它的原理,以及從什麼出發點被創造出來的,知道物件導向的根本概念後,後面我們解說 JS 原型的時候,就不會那麼不知所以然。
物件導向:繼承的概念
前面也提過原型存在的目的是為了達到繼承,那麼我們先來看看繼承是怎樣的概念,在物件導向裡的繼承是指類別可以以另一個類別為基礎,再往上進行擴充、或是修改,這樣一來就可以用很方便且較低成本的方式創造新的類別,因此,姑且說繼承的目的是為了讓「某些屬性可以共用」且可以減少重複。
用生活化的方式比喻的話繼承與被繼承物件之間的關係,有點像圖片內的「動物」這個總稱與「鳥」這樣更明確的稱呼,鳥也是動物的一種,所有動物都有特定共用的行為例如呼吸,但是有些行為可能只有鳥類做得出來例如飛行,因此可以知道,繼承可以讓物件同時具有共用的部分與較為特定的部分。
函式上的原型物件屬性
在物件導向裡面有類別的概念讓物件得以用很快速清楚的方式擴充,而 JS 裡面只有「物件」,所以只能用模擬的方式來達成類似的效果 — 那就要透過原型的幫忙。
前面在講繫結的時候我們提到,函式可以搭配 new
運算子成為「函式建構子」來產生物件,我們先來討論函式建構子的概念是什麼。在 JS 裡面,函式建構子其實與一般函式呼叫沒有差別,只是前面多了 new
這個關鍵字而已。
而在 JS 裡面,一個函式被創造出來的時候,JS 引擎會新增一個 prototype
屬性到這個函式上面,這個 prototype
是一個物件,我們姑且稱之為「原型物件」,在原型物件裡面我們可以找到一個指回該函式的constructor
屬性。
我們用下面的程式碼來當作例子:
function User(firstName, lastName) {
this.firstName = firstName,
this.lastName = lastName,
this.fullName = function() {
return this.firstName + " " + this.lastName;
}
}
var user1 = new User("Gin", "Tsai");
console.log(user1)
我們用 User
函式當作函式建構子來產生物件,這個函式上面會有一個 prototype
屬性,且他是一個物件,裡面有另外兩個屬性:
- 剛剛提到的
constructor
屬性,指向回該建構函式 ( User ) _proto__
屬性 ,裡面又是另外一個物件,這一點後面會再詳談
透過函式建構子產生的物件
那麼,當物件透過這個函式建構子被產生之後,會不會有什麼特別的地方呢?相對於 JS 引擎在 function 上面加上 prototype
屬性,在這個新生成的物件上則是會被加上一個 __proto__
屬性,這個屬性恰好是指向剛剛函式建構子的 prototype
物件。
User.prototype === user1.__proto__ //true
因此我們透過上面的例子可以得出這樣子一個結論:透過函式建構子生成的物件,其上面會有一個指向該物件所屬函式建構子 prototype
屬性的 __proto__
屬性,也就是該新生成物件的「原型」。
現在讓我們用同樣的方式創造第二個使用者 user2
,因為一樣都是透過函式建構子所產生的物件,因此在這個物件上照理說也會有一個 __proto__
屬性並指向產生這個物件的函式建構子上的原型物件,所以我們可以知道只要是透過函式建構子被生成的物件,他們之間都有一個共享的原型物件( prototype
),先知道這一點很重要。
總結:原型物件屬性
現在我們知道了被生成物件與建構函式之間的關係:
所有透過函式建構子生成的物件,都透過
__proto__
屬性與函式建構子上的prototype
屬性做連結,或是說共享這個屬性。
但是光知道這些還沒有辦法知道實際的應用,下一章節我們會介紹這個部分,就讓我們往下看看 JS 是怎麼透過原型來達到繼承以及減少相同函式宣告的重複性的。