Array Methods繁多,許多人常常使用時會在去 MDN 看文檔,但如果你仔細看會發現奇怪地方,比如說你想要去查map的語法,你實際會看到Array.prototype.map()

更奇怪的地方是,有些會有.prototype,有些沒有,當初我初學時百思不得其解。

其實這就跟今天主題的Prototypal inheritance有關,讓我們從一開始提到的mapArray Methods開始看起,寫一個簡單的例子:

1
2
3
4
5
const arr = [1, 2, 3, 4, 5];

const arr2 = arr.map((n) => n + 1);

console.log(arr2);

會發現說,我明明沒有寫這個方法,但是我可以用,why?
arr 裡面明明就只有 12345。這就是原型繼承的奧妙之處。

arr = [1, 2, 3, 4, 5]其實可以看成是arr = new Array([1, 2, 3, 4, 5])

其中Array就是內建的一個 Constructor Function,它自身的prototype會指向一個帶有 map(方法)跟其他一堆的方法的超大物件。

在這邊arr.map其實就會等於Array.prototype

意味著arr會連結到它的原型物件,所以它可以使用它的原型物件所有的方法,就是剛剛提到的超大物件,如下圖:

可以理解成,這個arrarray繼承了map方法,在arrarray本身其實是沒有去定義方法的,全部都在它的原型物件上了,我們可以確認這件事情是不是真的,來驗證吧。

證明這件事情 => 原型繼承

第一步是看arr.__proto__會不會跟Array.prototype相等

arr.__proto__代表的是 arr 這個 array 它的原型物件。
Array.prototype是 Array 這個 constructor 的原型。
結果是 true 那其實就代表他們是相同的。

1
2
const arr = [1, 2, 3, 4, 5];
console.log(arr.__proto__ === Array.prototype); // ture

第二步是看arr.map是不是跟Array.prototype.map相同。

這邊是為了要證明我上面所講的,arr.map其實就會等於Array.prototype這件事情。
結果是 true 那其實就代表他們是相等的。

1
2
const arr = [1, 2, 3, 4, 5];
console.log(arr.map === Array.prototype.map);

第三步是看arr.map會不會跟arr__proto__.map一樣。
繼承的概念,就是當這個物件找不到了,它會從它的上層原型物件去尋找,所以理論上當 arr 找不到它的 map 方法時,應該可以從arr__proto__這個 arr 的上層原型物件去拿到 map 方法。

結果還是 true,代表上面想法沒有錯誤。

1
2
const arr = [1, 2, 3, 4, 5];
console.log(arr.map === arr.__proto__.map);

最後得出了一個結論,arr 確實成功了繼承了原型。

那些沒有.prototype的方法

剛剛是講有的,現在來講沒有的。
這邊用Array.isArray()來舉例。

圖片來源:MDN

為什麼不是Array.prototype.isArray(),其實道理很間單,我的理解是因為不能確定我使用的對象是不是 Array,所以我不能使用 Array 的原型物件來繼承。

像是Array.isArray()這個方法就是用來判斷這個東西是不是 Array 的,所以結果可能是也可能不是,所以沒辦法斷定說就是 Array。

所以必須在寫的時候,不能使用.prototype的方式,要直接把 Array 給寫出來,我們來做個差異化比較:

1
2
3
4
5
6
7
8
9
// 有.prototype => Array.prototype.map()
const test = [1, 2, 3, 4];
// test.map就會等於Array.prototype
console.log(test.map((a) => a * 2)); // [2, 4, 6, 8]

// 沒有寫.prototype => Array.isArray()
const test2 = [1, 2, 3, 4];
// 判斷的東西,是放在參數裡面的概念。
console.log(Array.isArray(test2)); // true

Object.prototype

原型繼承還有一個有趣的地方,就是剛剛講解的那個是Array的原型,但就算是Array,它的頂點原型也不是Array.prototype,而是Object.prototype

也不只是Array而已,全部的內建原型的頂點通通都是Object.prototype

所謂的頂點就是,上面已經沒有東西,高處不勝寒,想要再繼續往上的話只看的到null而已。

所以我會理解成,在JavaScript中所有的繼承都是由Object.prototype繼承而來,然後我去查詢相關爬文,也有一些人這麼說:

All objects inherit the properties and methods of Object

這句話出處:JavaScript Prototypes and Inheritance – and Why They Say Everything in JS is an Object

意思是其實所有的物件都是繼承了 object 的屬性跟方法,null 不算,因為它沒有原型,只是充當原型鏈的最後一環。

增加更改原型繼承

除了昨天講到不要用Object.setPrototypeOf以及obj.__proto__去指定變換原型物件之外,還有也不要去增加更改原型繼承。

像是上面有提到的Array.prototype.map()方法,它是內建寫好的,但其實也可以自己寫一個方法塞進去,像是:

1
2
3
4
5
String.prototype.good = function () {
console.log(this + "good");
};

"apple".good(); // apple good

這好嗎? 這不好。

原因有幾個:

  • 假如是在跟別人協作,你弄了一個自己寫的方法放入自己的原型物件,然後自己繼承用的很開心,但是別人看到會一頭霧水,不知從何而來。
  • 因為原型物件是 global 的,很容易造成衝突,像是說有兩個函式庫都添加了 String.prototype.good 這個方法,就會造成前面蓋後面。

自己需要用的話,那就直接寫一個方法給自己用就好,其實不太需要把這個方法,弄給全部人都能繼承,因為大部分javascript覺得你需要的方法都已經幫忙內建寫好了。

總結

希望大家看完這篇有比較懂為什麼大部分方法都有加上.prototype,以及對於原型繼承的部分有更實務上的理解,之前比較偏向講原理,而實際有運用到原型繼承的我想就是這些族繁不及備載的內建方法了吧,以上就是今天的介紹。

reference

[1] Inheritance and the prototype chain