2018年7月14日 星期六

[Guide] JavaScript 中的餅乾壓模器:瞭解函式建構式(function constructor)

先前我們有說明如何使用 object literal,也就是大括號 {} 的方式來建立物件,但除此之外,我們還可以使用函式建構式(function constructor)的方式來建立物件。
你可能會好奇?平常使用大括號來建立物件很方便阿,為什麼還需要多一個什麼函式建構式的東西呢?函式建構式的好處在於,當我們有許多類似屬性的物件時,透過函式建構式可以很方便的為我們產生相同屬性內容或函式的物件。
當我們有許多類似屬性的物件時,透過函式建構式可以很方便的為我們產生相同屬性內容的物件。

什麼是函式建構式(function constructor)

不知道你有沒有看過麵包店賣的造型餅乾,你會發現這些餅乾的造型都很像,頂多只是餅乾上面的一些裝飾,像是薑餅人的眼睛、鬍子的特徵不太一樣而已,但是整體來說都還是一個人的形狀。之所以能夠做到這樣相似,是因為在製作餅乾的過程中有使用「壓模機」,也就是餅乾外框的模型,因為這些餅乾都套用了同樣的外框模型,所以每塊餅乾做出來的長像都很相似。
Photos credits at Free Food Photos
同樣的,你可以把函式建構式想像成餅乾的壓模機,只是餅乾壓模是用來快速地產生相似形狀的餅乾,而函式建構式則可以快速地產生相似內容的物件。
例如說,我們知道電話簿裡的好友通常都會有他們的名字、電話、地址等等,按照過去的方法,我們可以建立物件:
// 建立 Aaron 這個聯絡人
let aaron = {
    name: 'Aaron Chen',
    phone: '886930662442',
    address: '台北市五花路 3 段 2 號'
}

為什麼需要函式建構式

假設我沒什麼朋友,只有一個朋友叫 Aaron 那倒還好,只要寫一次就好,但如果我今天又認識了金秘書、C 羅等等,那我就要在重複寫:
let kim = {
    name: '김비서',
    phone: '82237897961',
    address: '首爾特別市中區明洞 173 街 227'
}

let ronaldo = {
    name: 'Cristiano Ronaldo',
    phone: '351964139955',
    address: 'Lua do Carmo 76 1anvar, 1200-094 Lisboa, 葡萄牙'
}
多寫兩次好像也還好,但如果我朋友很多,這樣我不就要重複寫很多次!!,如果哪天我又想幫所有的聯絡人加上生日、星座、血型這些項目的話,那不是麻煩死了。
因此透過函式建構式,我們就可以像餅乾的壓模一樣,快速方便的建立許多類似的的物件來使用。

建立與使用函式建構式

函式建構式(Function Constructor)這個名字中,有一個函式(function),說明了它是透過函式的方式來建立新的物件,基本的寫法像是這樣,我們先透過 function 來定義一個函式,裡面的參數則代入我們需要的物件屬性,像是 name, phoneaddress,另外也可以在這個函式中放入可能會用到的方法,例如撥打電話的功能:
// 函式建構式的寫法和一般建立函式的寫法相似

function Person (name, phone, address) {
  this.name = name;
  this.phone = phone;
  this.address = address;

  this.callPerson = function () {
    console.log(`你正在撥話給 ${this.name},電話號碼是 ${this.phone}`)
  }
}
這樣我們就寫好了一個名為 Person 的函式建構式,要怎麼使用它來產生物件呢?只要在呼叫這個函式的最前面加上關鍵字 new 就可以了
// 使用函式建構式的方式和呼叫函式相似,只需在呼叫函式前加上關鍵字 new

let kim = new Person('김비서', '82237897961', '首爾特別市中區明洞 173 街 227');
console.log(kim);
console.log(kim) 我們會得到一個物件,長這樣:
你可以看到,透過函式建構式產生的物件裡面就會包含了屬性 name, phone, address,另外也包含了 callPerson 這個函式,讓我們來呼叫看看:
kim.callPerson();
現在這個電話簿感覺起來非常方便,如果我們要新增聯絡人也很方便,不需要重複打一大堆的屬性,只需要:
let aaron = new Person('Aaron Chen', '886930662442', '台北市五花路 3 段 2 號');
let ronaldo = new Person('Cristiano Ronaldo', '351964139955', 'Lua do Carmo 76 1anvar, 1200-094 Lisboa, 葡萄牙');
這樣就又新增好了兩個聯絡人的物件,是不是很方便呢!
我們會把根據建構式(constructor)所建立出來的物件稱作是實例(instance)。因此在這裡 Person() 這個函式就稱作建構式(constructor),而透過它建立出來的物件,例如 kim, aaron, ronaldo 等就稱作實例(instance)
透過建構式(constructor)產生的物件被稱作實例(instance)

透過 new 到底做了什麼事?

瞭解了函式建構式的用法之後,讓我們來看一下這個過程發生了什麼事?
首先,在呼叫函式建構式,也就是這裡的 Person() 這個函式前,我們使用了 new 這個關鍵字。這個 new 是在 JavaScript 中眾多運算子(operators)的其中一種:
圖片來源:MDN Operator Precedence
當我們使用 new 這個關鍵字時,實際上會先有一個空的物件被建立
接著 People 這個函式會被執行(invoke),同時函式建構式裡面的 this 這個關鍵字會被指定成剛剛所建立的那個空物件
所以當執行 People 這個 function,執行到 this.name, this.phonethis.address 時,因為 this 現在指稱的是那個空物件,所以實際上是在幫這個空物件賦予屬性名稱和屬性值
若要更清楚的看到這個過程,我們可以在建構式的許多部分都放入 console.log(this) 來看看 this 發生了什麼事:
function Person (name, phone, address) {
  console.log(this);
  this.name = name;
  console.log(this);

  this.phone = phone;
  console.log(this);

  this.address = address;
  console.log(this);

  this.callPerson = function () {
    console.log(`你正在撥話給 ${this.name},電話號碼是 ${this.phone}`)
  }
}

let kim = new Person('김비서', '82237897961', '首爾特別市中區明洞 173 街 227');
結果會像這樣:
就如先前提到的,它會先產生一個空物件 {} ,而 this 會指稱到這個空物件,於是再把屬性一個一個放進去。在函式建構式中放入其他的函式(例如,this.callPerson)也是一樣的意思,只是把這個函式當成屬性值放到物件中去就是了。
要留意的是,在函式建構式的最後,不能用 return 指定要回傳其他內容,否則的話,它就不會傳回來物件給我們,而是只會回傳我們用 return 指定的內容。

重點留意與延伸閱讀

簡單來說其實函式建構式(function constructor)就是普通的 function,只是我們可以透過這個 function 來建立物件。在 function 前面加上 new 這個運算子,建立一個新的物件,並用函式中 this 來指稱這個空物件,如果你沒有在該函式的最後用 return 指定回傳出其它內容的話,它就會自動回傳這個新的物件給你。建構式的概念在物件導向(Object Oriented)的設計模式中經常被使用到。
但有幾點要特別留意的:
  • 一般來說,我們不會把函式寫在建構式中,也就是在這裡的例子中,直接把函式 callPerson() 放在建構式中並不是太好的作法,因為每一個建立出來的物件,裡面的 callPerson() 這個函式內容都是一樣的,比較好的方式是透過繼承的方式,所有透過這個建構式產生的物件都是去呼叫同一個函式,而不是每個物件裡都有一個同樣的 callPerson()。若想進一步瞭解繼承函式的方式可以參考 [筆記] 談談 JavaScript 中的 function constructor 和 prototype 的建立
  • 在 ES6 之後,可以使用 class 這個關鍵字來建立建構式,若有興趣可以進一步參考 JavaScript 類(Class) @ PJCHENder Unpublished Notes。

參考資料

0 意見:

張貼留言