前言
在前幾篇的筆記中,我們已經對於物件(object)
、原型(prototype)
、繼承(inheritance)
和原型鏈
等等有更多的了解,現在讓我們來更深入的談論一下 JavaScript中 建立物件的方法。
JavaScript語法本身其實已經有物件實體
這個快速好用的建立物件方法,但追溯到早期 JavaScript剛被創造出來時,參考了其他語言的古典繼承的概念和 new
關鍵字,而與 new
一起出現的用法就是 function constructor函數建構子
。
函數建構子 Function constructor
能用來新建物件的一種函式,透過與
new
運算子一起使用,能創建出新物件並設定該物件的屬性與方法
1 | function Person (){ |
結果
變數john
指向了new person()
創造出來的物件,印出john
發現裏頭的屬性和Person()
一致。
在運算子表格裡,可以得知new
是一種運算子。
補充知識 - instance 物件實體
由函數建構子創造出來的物件,稱為instance,由上例我可以說,物件 john
是函數建構子 Person
的物件實體。
當使用new時背後發生什麼事 ?
- JS
直譯器 syantax parsor
會先建立一個空物件{}
。 - 接著呼叫
new
後面的函式建構子
,當函式被呼叫時,創造函式執行環境 Excusion content
,this
關鍵字也隨之被創造出來。 - 由於
this
被寫在new
的後面,JS直譯器知道你在用函數建構子
創造物件,因此this
指向了剛被創造出來的空物件{}
,所以函式建構子
內的this.xxx
被創造在這個空物件中。
我們先前知道,全域函式的this
會指向全域物件,現在因為new
關鍵字,this
指向了john
,我們可以將程式碼改成這樣,多加一個console.log(this);
,看看會印出window
還是john
指向的這個物件。
1 | function Person (){ |
結果
由此可以了解
new
會改變this
的對象
如果在函數建構子使用 return
使用new
的狀況下,new
後方的函式預設不會回傳值,若故意在函式裡寫上return
的話,函式反而會因為return
回傳東西,依狀況決定是否把this
創造的物件屬性蓋掉,看看例子
retrun一個字串
1 | function Person (){ |
結果
雖然加了return 'this is return value'
但是物件輸出結果不受影響
retrun一個物件
1 | function Person (){ |
函式的最後若 return其他物件,則前面設定的物件內容會被覆蓋。
容易出錯的地方
需要注意的是,若是使用使用
函數建構子
不用new
,就會變成一般的呼叫函式。
1 | var john = new Person(); |
- 有用
new
- 建立物件,且 this指向變數
john
;
- 建立物件,且 this指向變數
- 忘了用
new
- 單純呼叫函式 Person,且這時 this指向全域物件,因此設定的屬性會被設定到全域物件(window)裡。
因此,如果使用函數建構子忘了放上 new可能會造成預期以外的錯誤,且它的樣子與一般的函數表示式 function ecpression長得無異,會非常難以 debug,因此人們習慣將所有函數建構子的首字以大寫表示
函數建構子和prototype的建立
函式就是物件,它有一些隱藏屬性,像是函數的名稱(Name)及函數的內容(Code),其中還有一個專屬於function
的屬性叫做.prototype
,這個屬性會以空物件形式呈現。
除非你是把 function 當做 function constructor 來使用,否則這個屬性就沒有特別的用途;但如果你是把它當做 function constructor,透過 new 這個關鍵字來執行這個 function 的話,它就有特別的意義了。
圖片來源: PJCHENder那些沒告訴你的小細節
要設定這個 function 的 prototype 屬性只要透過 .prototype
就可以了。
然而,有一點很容易讓人困惑的地方,我們會以為使用 .prototype
時,就可以進入這個函數的原型,但實際上則不是這樣。
函數當中的 prototype
屬性並不是指這個函數本身的 prototype
,他指的是透過這個函數建構子所建立出來的物件 __proto__
。
function 中的 prototype 屬性一開始是空物件
看看以下程式碼
1 | function Person(firstName, lastName){ |
到 Google Chrome 的 console 中,輸入Person.prototype
會得到一個空物件。
透過 function constructor 所建立的物件會繼承該 function 中 prototype 的內容
接著,讓我們在 Person.prototype
裡面增加一個 getFullName
的函數
1 | function Person(firstName, lastName){ |
我們為 Person.prototype 添加了一個函式,所以當我們在 Google Chrome 的 console 視窗中呼叫 Person.prototype 時,會多了這個函式在內:
剛剛,我們有提到很重要的一句話,「函式當中 prototype 這個屬性並不是這個函式本身的原型,它指的是所有透過這個它所建立出來之 物件實體的原型
」。
用程式概念可能比較好說明,這句話的意思是說 Person.prototype 並不是 Person.__proto__
,但是所有透過 Person 這個所建立的物件實體,在該物件實例的 __proto__
中,會包含 Person.prototype 的內容。
也就是說,當我們使用 new
這個運算子來執行函式建構式時,它會先建立一個空物件,同時將該建構子中 prototype,設置到該物件實例 john.__proto__
中。
因此,當我們在 Google Chrome 的 console 中輸入 john.__proto__
時,我們就可以看到剛剛在Person.prototype
所建立的函式 getFullName
已經繼承在裡面了。
實際運用
由於 Person.prototype
中的方法已經被繼承到由 Person
這個 function constructor
所建立的物件實例 john
中,所以這時侯,我們就可以順利的使用 john.getFullName
來呼叫這個方法。
1 | function Person(firstName, lastName) { |
如此,可以正確的執行 getFullName
這個函數並得到如下的結果
我們不該把方法放在 function constructor 中。
透過以上的方法,我們可以讓所有根據這個函式建構式 function constructor
所建立的物件都包含有某些我們想要使用的方法。如果我們有 1000 個物件是根據這個函式建構式所建立,那麼我們只需要使用 .prototype
這樣的方法,就可以讓這 1000 個物件都可以使用到我們想要執行的某個 method
。
有的人可能會好奇說,為什麼我們不要把 getFullName 這個方法直接寫在函式建構式當中呢?
把 method
放在函數建構子內雖然程式仍可執行,但是如果今天有1000個物件都是根據這個函數建構子所建立,那麼這1000個方法將會各自佔據記憶體空間,相反的如果建立在 prototype
中,就只會有一個記憶體空間被佔據。
所以,為了效能上的考量,最好的做法是 屬性 property
放在建構子當中, 方法 method
則放在 prototype
中。
原型繼承
沿用上一個例子,當我們希望有一個新的建構子 可以繼承另一個已存在的建構子的屬性及方法,能怎麼做
1 | function Person(firstName, lastName) { |
現在我們要創造一個新的建構子 Teacher,老師會有名字也會有姓,但和其他人不一樣的是,老師可能還包含教授科目的資訊
1 | function Teacher(firstName, lastName, subject) { |
設定 Teacher() 的原型與建構子參考
到這部其實還沒結束,目前 Teacher 所繼承的只有來自 Person 的屬性 ( firsName 和 lastName ),還沒有繼承來自 Person 的方法 (getFullName)
來看看目前 Teacher 的原型確實還沒有 getFullName 方法
我們透過 Object.crete()
並搭配等同於 Person.prototype 的原型,建立新的 prototype 屬性值 (它本身就是物件,包含屬性與函式) ,並將之設定為 Teacher.prototype 的值。也就是說 Teacher.prototype 現在會繼承 Person.prototype 上的所有可用函式
1 | Teacher.prototype = Object.create(Person.prototype) |
Object.create() 詳細的使用方法可以參考
現在 Teacher 的原型鏈中已經可以找到 getFullName 方法了!
到目前為止看起來好像很完美了,但其實還沒有結束,現在如果試著輸入
1 | Teacher.prototype.constructor |
這樣可能會產生問題,所以要設定正確。你可回到自己的原始碼並在最下方加入下列程式碼:
1 | Teacher.prototype.constructor = Teacher |
到這就算是完整的做完了一個原型繼承的流程
如果要給 Teacher 建構子擴充新的函式 …
1 | Teacher.prototype.greeting = function() { ... } |
ES5 原型繼承 DEMO
補充
建立物件實體後,再在函數建構子裡新增方法
1 | function today(food1,food2){ // new出物件前設定 |
結果
即使在使 new創造一個物件之後,才使用.prototype
創造eat2()
,物件實體的__proto__
中依然可以找到該屬性,而這和JS的傳參考特性有關。
封裝成函數,避免在使用函數建構子時忘記寫new
在上面容易出錯的地方
提到,在使用函數建構子如果忘了在前面加上 new
可能會造成一些很難發現的問題。這邊提供一個方式可以有效避免這個情況。
1 | function A(name, age) { |
我們設定了一個函數 $
,在這個函數中把創造物件的動作 new
在裡面執行完畢後並且回傳,如此一來我們就不需要在每次創造物件實體 instance的時候都加上一次new
,也可以不用擔心忘了加上new
產生不好的結果。
1 | var person = $('dylan', 18); |