前言
JavaScript 作為一個現今非常熱門的語言,其撰寫 OOP (Object Oriented Programming) 的方式在 ES6 的 class
語法糖出現之前使用的是以 function 為基底的 prototype 寫法,這種寫法應該讓不少以 Java 或 C++ 起步的人為之納悶,還看過有人稱 prototype 是「殘缺」的物件導向寫法。
實際上 prototype-based programming 與 class-based programming 是兩種不同的派別,但是都屬於 OOP 的子系統,後來因為 class-based 的語言幾乎成為主流,在 JavaScript 的 ES6 版本中才新增了 class
的語法糖,而根基仍然是 prototype。
今天我們不談兩種不同派別的 OOP 子系統差別,而是試著了解在 JavaScript 中的 new
究竟做了哪些事情,你也許會好奇當我們使用了 new User()
的語法後,JavaScript 究竟做了什麼,接下來就讓我們依序來了解吧!
快速了解 prototype
在 ES6 之前的 JavaScript 沒有 class
語法糖,建立物件都是用 function
宣告一個 constructor
,然後在 constructor
的 prototype
原型鍊上新增屬性或方法。
以下我們先用一個簡單的例子快速理解 prototype 的物件實例,首先宣告了一個 User
,並且傳入了一個 name
初始化 User
物件;接著在 User
的 prototype
上新增 hello()
回傳使用者的名稱。
然後就是大家熟悉的 new User('Peter')
,實例化後我們就可以調用在 prototype
上定義的屬性以及方法。
以下為我們在瀏覽器中印出 user
的結果,可以看到 user
底下的屬性只有 name
,而 hello()
被存在 __proto__
中。
雖然在 user
的物件中我們並未看到 hello()
這個方法,但是仍然能夠使用 user.hello()
呼叫方法的原因在於 Prototype Chain。簡單來就說就是當我們在當前的物件中找不到其方法或屬性時,會往繼承上的物件找,也就是 __proto__
上的物件,所以在第一層的 __proto__
找到了 hello()
這個方法,最後才能順利呼叫 user.hello()
。
我們可以統整一下上述
new User()
後發生的事情。
在 constructor
中,也就是 function User() {}
中所定義的 this.xxx
會放在 User
的第一層,而其餘定義在 prototype 上的物件則是以繼承的形式被放在 __proto__
上。
而 new User()
會回傳這個物件的 reference,讓我們可以進一步的使用這個物件上的方法與屬性。
從零開始實現一個 new operator
🔗 MDN 的文件已經整理好 JavaScript 中 new operator 的實際上執行的 4 個步驟:
- 創造一個空物件
- 將
constructor
鏈結到所創造的空物件上 - 將第一個步驟創造的物件作為 this 傳遞給
constructor
- 如果該
constructor
沒有回傳物件,則回傳所創造的物件
以下我們拆成 5 個部分來看實作
new
的過程。
👉 _new function
首先,我們先宣告一個 _new
方法,這個方法第一個參數為 constructor
,其餘的參數為 constructor
的參數,像是 new User('Peter')
,使用我們定義的方法會像是 _new(User, 'Peter')
。
接著,我們來實作 _new
方法的程式碼。
👉 創造一個空物件
第一個步驟很簡單,就跟大家想得一樣,給予一個變數為空物件 {}
。
👉 將 constructor
鏈結到所創造的空物件上
第二個步驟也是大家常常看到的繼承一個物件時會看到的範例。
❗❗但是 __proto__
是一個非正規的用法,根據 MDN 的官方文件說明,在過去 ES6 以前 __proto__
是沒有定義在 ECMAScript 的 spec 中,但是許多瀏覽器卻實做了這個屬性,因此在 ES6 中 __proto__
被納進了 spec。
可是這個 __proto__
會影響到 JavaScript 的執行效能,我們不應該在 production 的環境上使用它。
以上創立空物件與繼承 constructor 兩個步驟,我們可以將它們合併為一行指令,使用 Object.create
可以繼承傳入的 constructor,與使用 __proto__
是類似的行為。
而你們現在知道了 new
operator 跟 Object.create
其實都做了繼承物件這件事,那麼兩者有什麼差別呢?
new operator 跟 Object.create 的差別在於會不會執行 constructor
你們可以把以下程式碼拿到可以執行 JavaScript 的環境測試看看,你們會發現只有 new User()
會印出 'user'
這個字串,而 Object.create
並不會執行 User()
中的程式碼。
👉 將第一個步驟創造的物件作為 this
傳遞給 constructor
第三個步驟是使用 apply
,將 obj
作為 constructor
中 this
的參考,並將所有會用到的參數 args
傳入 constructor
。
這個方法跟 call
有點類似,第一個參數都是傳入 this
參考的物件,差別在於 apply
的第二的參數是一個陣列,作為方法的所有參數;而 call
同樣是放方法的參數,但是是以位置對應的形式,如同平時我們呼叫方法一樣。
👉 如果該 constructor
沒有回傳物件,則回傳所創造的物件
第四個步驟看起來也很簡單,在 return
時用 typeof
判斷要回傳哪個物件,但是我其實挺納悶的這個設計好像沒什麼用?
我們來看一個範例,我們在 function User() {}
中加上 return
一個物件,然後用 new User('Bob')
實例化這個物件,呼叫 user.hello()
時會出現 TypeError: user.hello is not a function
。
原本我以為 new
會忽略 constructor
回傳值,不論如何回傳的都是一個物件完整的 reference,如此一來才能讓我們去操作這個物件。但是,JavaScript 的 new
卻會因為 constructor
有沒有 return
決定 new
的回傳值,實在有點匪夷所思的設計。
但也許是我功力不夠,還不明白這個設計有什麼好處,如果知道的話,歡迎各位大大留言告訴我 😅。
Recap
要實作一個 new
operator 可以參考一下 4 個步驟 (取自 MDN 文件):
- 創造一個空物件
- 將
constructor
鏈結到所創造的空物件上 - 將第一個步驟創造的物件作為 this 傳遞給 constructor
- 如果該
constructor
沒有回傳物件,則回傳所創造的物件
結論
這篇文章我們了解到 new
operator 的運行原理與實作過程,如果是第一次看到 new
的實作,應該也不會覺得它很複雜,反倒應該會覺得:「就 John ?」
而我們實作的 _new
只能用在 function-based 的物件,沒辦法用在 class
定義的物件上,因為 class
物件要求在實例化時必須要用 JavaScript 定義的 new
,否則會出現錯誤 TypeError: Class constructor cannot be invoked without 'new'
。
分享就到這邊,如果喜歡我的文章可以幫我拍個幾下手,在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。 😃