new operator — JavaScript | 為了瞭解原理,那就來實作一個 new 吧!

Leo Chiu
手寫筆記
Published in
8 min readAug 30, 2020
Photo by Fezbot2000 on Unsplash

前言

JavaScript 作為一個現今非常熱門的語言,其撰寫 OOP (Object Oriented Programming) 的方式在 ES6 的 class 語法糖出現之前使用的是以 function 為基底的 prototype 寫法,這種寫法應該讓不少以 Java 或 C++ 起步的人為之納悶,還看過有人稱 prototype 是「殘缺」的物件導向寫法。

實際上 prototype-based programmingclass-based programming 是兩種不同的派別,但是都屬於 OOP 的子系統,後來因為 class-based 的語言幾乎成為主流,在 JavaScript 的 ES6 版本中才新增了 class 的語法糖,而根基仍然是 prototype。

今天我們不談兩種不同派別的 OOP 子系統差別,而是試著了解在 JavaScript 中的 new 究竟做了哪些事情,你也許會好奇當我們使用了 new User() 的語法後,JavaScript 究竟做了什麼,接下來就讓我們依序來了解吧!

快速了解 prototype

在 ES6 之前的 JavaScript 沒有 class 語法糖,建立物件都是用 function 宣告一個 constructor,然後在 constructorprototype 原型鍊上新增屬性或方法。

以下我們先用一個簡單的例子快速理解 prototype 的物件實例,首先宣告了一個 User ,並且傳入了一個 name 初始化 User 物件;接著在 Userprototype 上新增 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 個步驟:

  1. 創造一個空物件
  2. constructor 鏈結到所創造的空物件上
  3. 將第一個步驟創造的物件作為 this 傳遞給 constructor
  4. 如果該 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 作為 constructorthis 的參考,並將所有會用到的參數 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 文件):

  1. 創造一個空物件
  2. constructor 鏈結到所創造的空物件上
  3. 將第一個步驟創造的物件作為 this 傳遞給 constructor
  4. 如果該 constructor 沒有回傳物件,則回傳所創造的物件

結論

這篇文章我們了解到 new operator 的運行原理與實作過程,如果是第一次看到 new 的實作,應該也不會覺得它很複雜,反倒應該會覺得:「就 John ?」

而我們實作的 _new 只能用在 function-based 的物件,沒辦法用在 class 定義的物件上,因為 class 物件要求在實例化時必須要用 JavaScript 定義的 new ,否則會出現錯誤 TypeError: Class constructor cannot be invoked without 'new'

分享就到這邊,如果喜歡我的文章可以幫我拍個幾下手,在閱讀文章時如果有遇到什麼問題,或是有什麼建議,都歡迎留言告訴我,謝謝。 😃

--

--

Leo Chiu
手寫筆記

每天進步一點點,在終點遇見更好的自己。 Instragram 小帳:@leo.web.dev