原型Prototype: [].map的map是哪來的?

prototype-01

前言

以前剛接觸到陣列map的時候,覺得很開心不用一直寫for迴圈,但我師父問起:你知道怎麼會有這個嗎?
然後就開始解釋起原型鏈這可怕的東西了…
在這裡只會提到原型繼承的概念,並不會挖太深。

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

arr.map(x => x + 1) // [2,3,4]
arr.filter(x => x > 1) // [2,3]
arr.length // 3
prototype-01

不只是map,還有許多方便的方法,在建立物件的時候,就跟著繼承下來了

函式建構子 Function constructor

從建立一個物件開始吧!

如果我今天要建造一個人?

1
2
3
4
var person = {
name: 'Roman',
description: 'Cool',
}

物件實體法也是最主流的方式,宣告一個人很酷的人做Roman。

但如果今天我需要快速得到很多不同名字的人呢?

函式回傳物件

1
2
3
4
5
6
7
8
9
function getPerson (name, description) {
return {
name: name,
description: description
};
};

var roman = getPerson('Roman', 'Cool');
var corgi = getPerson('Corgi', 'Cute');
prototype-02

目前為止沒什麼問題,但有點囉嗦。如果早知道要建立物件的話,又何必要建立新物件再回傳呢?也為了往後其他目的,JavaScript 透過函式建構子提供了方便的捷徑。

函式建構子

1
2
3
4
5
6
7
function Person (name, description) {
this.name = name;
this.description = description;
}

var roman = new Person('Roman', 'Cool');
var corgi = new Person('Corgi', 'Cute');
prototype-02

函式建構子名稱往往以大寫起頭,可方便你在程式碼中找出函式建構子或誤用。

一個是呼叫執行函式並將結果指向變數roman,一個是用new建立物件並指向變數roman;

當使用new時背後發生什麼事 ?

  1. JS直譯器syantax parsor會先建立一個空物件{}。
  2. 接著呼叫new後面的函式建構子,當函式被呼叫時,創造函式執行環境 Excusion contentthis關鍵字也隨之被創造出來。
  3. 由於this被寫在new的後面,JS直譯器知道你在用函式建構子創造物件,因此this指向了剛被創造出來的空物件{},所以函式建構子內的this.xxx被創造在這個空物件中。

new會改變this的對象(原指向全域物件)

在函式建構子中 return

1
2
3
4
5
6
function Person (name, description) {
this.name = name;
this.description = description;
return 'this is return value';
}
var roman = new Person('Roman', 'Cool');
prototype-03

回傳字串不會受影響

1
2
3
4
5
6
function Person (name, description) {
this.name = name;
this.description = description;
return {name: 'No corgi anymore'};
}
var corgi = new Person('Corgi', 'Cute');
prototype-04

回傳物件會把創立的新物件覆蓋掉

別忘記new

若使用使用函數建構子不用new,就會變成一般的呼叫函式。

1
2
var roman = new Person('Roman', 'Cool');
var roman = Person('Roman', 'Cool');

Prototype 原型

重複用的函式

1
2
3
4
5
6
7
8
9
10
function Person (name, description) {
this.name = name;
this.description = description;
this.greeting = function(){
return "Hi, I'm " + this.name;
};
};

var roman = new Person('Roman', 'Cool');
var corgi = new Person('Corgi', 'Cute');
prototype-06

我加了一個打招呼的方法,在新創建的物件都使用這個方法

假設每個新創立的物件都會用到greeting

prototype-06

從圖中可以看到,看起來是同樣的函示,但事實是greeting分別存在兩個新建立的物件。

prototype-07

說明如果我建立10個新物件,就會多10個存放greeting的空間。

如果不是每個新物件都需要用到,就等於某些物件多建立了greeting

浪費空間耗能。

放在工具包(繼承)

1
2
3
4
5
6
7
8
9
10
11
function Person (name, description) {
this.name = name;
this.description = description;
};

Person.prototype.greeting = function(){
return "Hi, I'm " + this.name;
};

var roman = new Person('Roman', 'Cool');
var corgi = new Person('Corgi', 'Cute');
prototype-05

Personprototype屬性,可以把它想成是Person的工具包,每個function都有的屬性,被創立出來時是空的,可以使用prototype幫這個物件(工具包)擴充。

prototype-08.png

而用new創建新的Person時,會把它繼承過去,物件本體雖然沒有這個greeting,但沿著工具包找是可以取用到的。

prototype-09.png

兩個物件的greeting都指向同一個參考物件,記憶體上是同一個

所以說那個map呢?

有了剛剛那些概念,現在談談當我們宣告一個陣列時,發生的事情

1
var arr = [1,2,3];

當我今天宣告一個陣列時,其實就把當使用new時背後發生什麼事 ?提到的事情做完了

1
2
3
var arr = [1,2,3];
// 因為coercion 效果等於下面這個
var arr = new Array(1,2,3);
  1. 宣告創造一個新的空陣列物件
  2. 將建構子的this指向過去
  3. 把工具包也傳承下去(但不存在陣列本體)

所以就有了開頭這張圖

prototype-01

陣列也算是物件的一種’

prototype-09

驗證

關鍵字:coercion強制轉型

小結

這篇本來是想把原型的都寫完,但秉持著每篇文章儘可能淺顯易懂,儘可能不要有太多的延伸討論,不要偏離標題太多,先簡述概念,之後在陸續把原型的理論補齊。

參考資料

[Peter Chang: Javascripter 必須知道的繼承 prototype, [[prototype]], __ proto __](https://peter-chang.medium.com/javascripter-必須知道的繼承因子-prototype-prototype-proto-object-class-inheritace-nodejs-物件-繼承-54102240a8b4)
Max’s Blog: 面試官最愛考的 JS 原型鏈
SimonAllen: 古典與原型繼承、瞭解原型
MDN: 繼承與原型鏈
Summer: 你懂 JavaScript 嗎?#19 原型(Prototype)
huli: 該來理解 JavaScript 的原型鍊了
[筆記] 談談 JavaScript 中的 function constructor 和關鍵字 new
MDN: 初學者應知道的物件導向 JavaScript