JavaScript 核心觀念(48)-繼承與原型鍊 - 原型在哪裡?

前言

接下來這一章節將會開始正式的介紹原型哦。

原型

原型

一開始我們先這樣子舉例好了,當我們定義一個狗的物件是會像下面這樣

1
2
3
4
5
6
7
var bibi = {
color: '棕色',
size: '小',
bark: function() {
console.log('吠叫');
}
};

那麼當我們要取得任一屬性時就會使用點運算子(or 中括號)來取值,而這一段是我們常見的 JavaScript 物件。

基本上這邊要強調的一件事情終究還是「JavaScript 是由一個物件所組合而成的」,而原型的概念也非常雷同,只是將一隻狗基本會有的屬性變成了原型,而在此原型的概念還是閃不掉物件

1
2
3
4
5
6
7
var 狗(的名字) = {
color: 狗的顏色,
size: 狗的體型,
bark: function() {
console.log('吠叫'); // 狗都會做的行為
}
};

因此狗要建立實體(賦予生命)時,就會基於狗的原型去建立,因此它就會是兩個物件繼承下來的概念。

而原型基本上也會繼承到另一個原型,而另一個原型也會有自己的屬性與方法,而在此原型就會一段一段向上查找,而這一段又稱之為原型鍊

原型鍊

假使若使用的方法是原本的實體內沒有的方法時,那麼該實體透過原型鍊向上,而這個過程它就會不停的向上查找,直到找到頂為止,概念與範圍鍊非常像

向上查找

這邊也額外小提一下,假使有兩個實體都是基於同一個原型所建立的話,那麼這些實體也會尋找同一個原型。

原型的特性

因此這邊列一下原型基本的特性是什麼

  • 一樣具有物件的特性
  • 向上查找特性
  • 原型可共用方法與屬性

那麼接下來直接透過實際的範例會比較清楚

1
2
var arr = [1, 2, 3];
console.log(arr);

而在此實體是什麼呢?其實 arr 就是一個實體,那麼原型在哪呢?基本上你貼上程式碼後展開,可以在裡面找到一個 「__proto__」,而這就是陣列 (Array) 的原型,而這裡面會有非常多陣列的方法,我這邊就不再次截圖,有興趣的可以自己嘗試執行看看。

回到原型這邊,我們有講到實體可以取用原型本身的屬性與方法,因此我們就可以使用 arr.forEach 這類常見的方法

1
2
3
4
5
6
var arr = [1, 2, 3];
console.log(arr);

arr.forEach(function (item) {
console.log(item);
})

接下來我們可以在驗證一個東西,也就是「原型的方法與屬性是共用的」,因此我們在宣告另一個陣列時,相同可以使用一樣的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [1, 2, 3];
console.log(arr);

arr.forEach(function (item) {
console.log(item);
})

var arr2 = [4, 5, 6];
console.log(arr2);

arr2.forEach(function (item) {
console.log(item);
})

但光看上面一定感覺不出所以然,那我們該如何驗證「原型的方法與屬性是共用的」呢?在此可以這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var arr = [1, 2, 3];
console.log(arr);

arr.forEach(function (item) {
console.log(item);
})

var arr2 = [4, 5, 6];
console.log(arr2);

arr2.forEach(function (item) {
console.log(item);
})

arr.__proto__.forEach === arr2.__proto__.forEach; // true

當然也可以透過直接針對原型增加方法來驗證

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var arr = [1, 2, 3];
console.log(arr);

arr.forEach(function (item) {
console.log(item);
})

var arr2 = [4, 5, 6];
console.log(arr2);

arr2.forEach(function (item) {
console.log(item);
})

arr.__proto__.forEach === arr2.__proto__.forEach; // true

arr.__proto__.myName = function() {
console.log('Ray is Me!');
}

arr.__proto__.myName === arr2.__proto__.myName; // true

arr.getData(); // Ray is Me!
arr2.getData(); // Ray is Me!

在此要注意上方新增原型的方式 arr.__proto__.myName 只是一個範例,實際開發上請不要這樣子建立原型,這樣建立原型將會導致無法確定原型的來源,而導致原型出現錯誤。

這邊額外講一下,其實原型本身也是有層級的,只是它一直向上查找,基本上如果你點開 Arrar 之後再點一次 __proto__ 可以看到這個 __proto__ 會指向到 Object,因此這一整個陣列的原型都是基於物件原型繼承下來的,而物件原型你會找不到 __proto__ 其主要原因是,物件原型就是原型的最頂層的原型內容。

接下來來做的好玩的實驗,剛剛有講到物件原型是所有原型的頂層,因此如果你建立一個物件並直接針對該物件賦予一個方法會怎麼樣?

1
2
3
4
5
6
7
8
9
10

var obj = {
myName: 'Ray',
}

obj.__proto__.getName = function() {
console.log(this.myName + ' is Goods!');
}

obj.getName();// Ray is Goods!

剛剛我們有講過「物件原型是所有原型的頂層」,因此若你在陣列下直接輸入 getName 這個方法,其實是找得到的,只是無法正常顯示而已

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
myName: 'Ray',
}

obj.__proto__.getName = function() {
console.log(this.myName + ' is Goods!');
}

obj.getName();// Ray is Goods!

var arr = [1, 2, 3];
arr.getName();// undefined is Goods!

因此在一次驗證原型屬性都是共用的,而在此不建議使用 __proto__ 新增方法或屬性,其實也是因為會導致開發時無法知道原型從哪來,除此之外也會導致原型變得非常髒,因此實際開發上就會建議使用 prototype 以避免污染到一整個原型,只是在教學或說明原型觀念時,通常就會使用 __proto__ 以便在理解上太過複雜。

而之所以建議使用 prototype 在於建立的屬性就只會掛載該原型上,而不會無意識的污染的其他原型,而 prototype 只會存在於 function 上唷

1
2
3
4
5
6
7
8
9
function a() {
...
}

console.log(a)// 可以看到 prototype

a.prototype.getlast = function() {
return this[this.length - 1];
}

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ