# [JavaScript] 閱讀 Constructors Are Bad For JavaScript 後的整理 ###### tags: `前端筆記` ## 為什麼要讀這篇? 因為我目前正在學習 [FACTORY FUNCTIONS AND THE MODULE PATTERN](https://www.theodinproject.com/paths/full-stack-javascript/courses/javascript/lessons/factory-functions-and-the-module-pattern#factory-function) 文章的最後有自我檢核,但我不知道該怎麼回答第一題:*Describe common bugs you might run into using constructors.* 點了連結後頁面被導向這篇文章 [Constructors Are Bad For JavaScript ](https://tsherif.wordpress.com/2013/08/04/constructors-are-bad-for-javascript/),我覺得讀完大概就可以理解了,所以決定花時間好好消化。 ## 先來介紹建構子(constructor) ### 使用時機 如果想要建立很多擁有相通屬性的物件,可以先建立一個建構子(constructor)當作模型的藍圖,之後再搭配 `new` 關鍵字來新增實例物件(instance object)。因為實例物件(instance object)是以建構子為藍圖製造,所以就會有該藍圖的屬性(property)。 ```javascript= // 建構子 function Person (name, age, hasPet) { this.name = name; this.age = age; this.hasPet = hasPet; } const lun = new Person ('lun', 23, false); // Person{name: 'lun', age: 23, hasPet: false} ``` ### 如果有方法呢?要在建構子裡面寫嗎? 可以,但是這樣子會讓每個實例物件(instance object)都自己擁有自己的方法,這樣子只是一直重複而已,沒有「繼承」。 ```javascript= function Person (name, age, hasPet) { this.name = name; this.age = age; this.hasPet = hasPet; this.sayHi = function () { console.log(`${name} says Hi!`); } } const lun = new Person('lun', 23, false); const meimei = new Person('meimei', 25, false); ``` 兩個物件擁有「自己」的 `sayHi` 方法(method) ![](https://i.imgur.com/k4Oehv8.png) ==另一種活用「繼承」的方式== ```javascript= function Person (name, age, hasPet) { this.name = name; this.age = age; this.hasPet = hasPet; } Person.prototype.sayHi = function() { console.log(`${this.name} says Hi`); } const lun = new Person('lun', 23, false); ``` 方法「被繼承」了,而不是物件本身擁有。 ![](https://i.imgur.com/ItbWYMn.png) >所以方法不要寫在建構子(constructor)裡,因為當物件多的時候就會造成資源浪費。 ==推薦使用 `Object.create()` 這樣子就不用寫藍圖,直接繼承一個物件== ```javascript= const Person = { name: null, age: null, hasPet: false, greeting: function () { console.log(`${this.name} says Hi!`); } } const lun = Object.create(Person); ``` 儘管 `lun` 在一開始的時候本身沒有屬性,但因為「繼承」的關係可以直接使用繼承得到的屬性。之後 `lun` 有自己的屬性也不會影響繼承原本的東西(也就是 `Person`)。 ![](https://i.imgur.com/9ybF2ti.png) ## 回到文章,為什麼建構子(constructor)不好呢? ### 1. 如果忘記用 `new` 很危險 #### 先回來談談使用建構子時,`new` 到底做了蝦米? *範例參考([ref.](https://tsherif.wordpress.com/2013/08/04/constructors-are-bad-for-javascript/))* ```javascript= function C() { this.instance_member = "whoops!" } const c = new C(); // JavaScript 是這樣子看待上面的代碼的 const c = {}; C.call(c); ``` `new` 讓人類不用自己用 `Function.prototype.call()` 使實例物件(instance object)的 `this` 可以不會回到預設指向 `window` / 在 Node.js 則是 `global`。 但如果不小心忘記加 `new` 會怎麼辦? ```javascript= function C() { this.instance_member = "whoops!" } const c = C(); // Forgot "new" c; window.instance_member; // Property added to global namespace! ``` 就會發現==沒有報錯==,而且 ==`this.instance_member` 就回到預設 `this` 指向 `window` / 在 Node.js 則是 `global`==。 ![](https://i.imgur.com/rfy56wp.png) ### 2. 如果改了建構子(constructor)的 `prototype`,之後依照此建構子(constructor)創建的實例物件(instance object)的 `prototype` 就會出現問題 #### 首先要探討要怎麼 A 是否為 B 的實例物件(instance object)?也就是說要怎麼知道誰繼承誰呢? >The instanceof operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object. The return value is a boolean value. >`instanceof` 依建構子內的 `prototype` 屬性為基準,如果在實例物件中找到與建構子相同的 `prototype` 就會回傳 `true` => A 是 B 的實例物件;回傳 `false` 則代表 A 不是 B 的實例物件。 ```javascript= function A () {} function B () {} const c = new A(); // c 為 A 的實例物件 c instanceof A; => true // c 並不是繼承 B c instanceof B; => false ``` #### 所以改了建構子的 `prototype` 對實例物件會有什麼差別嗎? 這邊先說明一個概念: by default 宣告函式時,函式的 `prototype` 就會初始化新增一個叫做 `constructor` 的屬性並指向函式本身。而參照建構子新增的物件實例的 `prototype` 就會有從建構子繼承的 `prototype`。 所以這也就是為什麼要把方法寫在 `prototpye` 中,這樣子實例物件在叫用時,實例物件本身抓不到,但因為可以從 `prototype` 抓,所以最後成功叫用。 所以會到問題本身,到底會出現什麼問題呢? ##### *範例1.[ref.](https://tsherif.wordpress.com/2013/08/04/constructors-are-bad-for-javascript/)* 已知實例物件的 `prototype` 是繼承建構子的 `prototype` 如果產生完再更改建構子的 `prototype` 那麼實例物件會有影響嗎? ==答案是要看怎麼改 XDDD。== What the hell is going?????? 回到之前的筆記 [pass by value(傳值)、pass by reference(傳址)還是 pass by sharing](https://hackmd.io/2XXlJQ67RMa9Ekh9nU0NJg?view) 有學習到==基本型別是不能變的,但是物件型別可以==,所以如果是使用.(dot notation)就可以改變實際物件的值,但是如果是用物件實字(object literal)也就用花括號({})則是直接在 `Call Stack` 及 `Heap` 新增新的記憶體,因為對 JavaScript 來說物件實字(object literal)不是修改,是創建一個新的物件。 ```javascript= // Constructor function C() {} // Create object. const c = new C(); c instanceof C; => true c.constructor === C; => true // Change prototype C.prototype = { prototype_prop : "proto"}; c.constructor === C; => true c instanceof C; // instanceof no longer works! => false ``` 可以稍微簡單的把記憶體指向圖畫出來 ![](https://i.imgur.com/EAH1vTH.png) 因為 `instanceof` 是以建構子的 `prototype` 為基準在實例物件查找有無與建構子相同的 `prototype`。因為修改 `C.prototype` 是用物件實字(object literal),所以即便肉眼看跟流程都顯示 `c` 確實是 `C` 的實例物件,但是當用 JavaScript 提供的工具 `instanceof` 就會得到 `false`,因為 `c.prototype` 已經和 `C.prototype` 指向不同的物件了。 ##### *範例2.[ref.](https://tsherif.wordpress.com/2013/08/04/constructors-are-bad-for-javascript/)* ```javascript= // 原本 C 的 prototype 中會有屬性 constructor 並指向 C 本身 function C() {} // 但後來把 C 的 prototype 被改寫了 C.prototype = { prototype_prop : "proto"}; const c = new C(); // instanceof 會從建構子找 prototype,並在實例物件檢查有沒有相同的 prototype // 因為 c 是 C 的實例物件,所以當 C 的 prototype 被改寫,c 的 prototype 繼承了改寫的 prototype c instanceof C; => true // 所以 c.constructor 就會回去預設值(不是undefined)喔 c.constructor === C; => false ``` ![](https://i.imgur.com/3u6vI5r.png) ![](https://i.imgur.com/gJGgAhH.png) ##### *範例3.[ref.](https://tsherif.wordpress.com/2013/08/04/constructors-are-bad-for-javascript/)* ```javascript= // Create two constructors with the // same prototype. const proto = {protoprop: "protoprop"}; function C() { this.cprop = "cprop" }; C.prototype = proto; function F() { this.fprop = "fprop" }; F.prototype = proto; const f = new F(); f.protoprop; // Has prototype properties => "protoprop" f.fprop; // Has F properties => "fprop" f.cprop; // Doesn't have C properties => undefined f instanceof C; // Is an instance of C!?! => true ``` 因為此時 `C.prototype` 跟 `f.prototype` 是指向相同的記憶體,所以依照 `instanceof` 的規則(只要從實例物件中找到建構子的`prototype` 就 `true`)就會回傳 `true`,即使代碼根本沒這樣子寫。 ![](https://i.imgur.com/VBMAmjX.png) > c instanceof C does not mean that c was created by C or that c has anything to do with C. It basically just means “at this moment, the prototype C will use if it’s invoked as a constructor (even if it’s never actually invoked as a constructor) appears somewhere in the chain of prototypes of c”. Essentially, it’s equivalent to C.prototype.isPrototypeOf( c ), but the latter is far more upfront about what it’s actually doing. > 更清楚地來說,當 `c instanceof C` 回傳 `true` 並不代表 C 創造了 c,只代表在此刻(也就是寫這段代碼的時間)C.prototype 指向的記憶體剛好和實例物件 c 的不知道哪個環節的 `prototype` 是相同的而已。 ### Describe common bugs you might run into using constructors. 1. 如果在創造實例物件的時候忘記 + `new` JavaScript 不會報錯,但是 `this` 會回到預設指向 `window`(在瀏覽器)/`global`(在 Node.js) - 可以解決,但超麻煩! ```javascript= function C () { // 如果叫用 this 的物件執行 instanceof C 為 false // 該物件的 prototype 找不到和指向建構子的屬性 // => 重新 return if (!(this instanceof C)) { return new C(); } this.isHappy = true; } const a = C(); ``` 2. 如果建立實例物件後又「重新」物件實字(object literal)建構子的 `prototype` 話,使用 `instanceof` 確認彼此是否為建構子 / 實例物件的關係時會出現問題(可往上查看範例)。(這樣子很麻煩,有時候專案大不小心搞壞建構子的 `prototype` 很難發現) 3. 手動更改建構子的 `prototype` 會打破原本實例物件的 `constructor` 自行指向建構子函式規則。 ![](https://i.imgur.com/IBLzOkL.png) ## 參考資料 1. [Constructors Are Bad For JavaScript](https://tsherif.wordpress.com/2013/08/04/constructors-are-bad-for-javascript/) 2. [[筆記] 談談 JavaScript 中的 function constructor 和 prototype 的建立](https://pjchender.blogspot.com/2016/06/javascriptfunction-constructorprototype.html) 3. [JS基本觀念:typeof vs instanceof](https://medium.com/@mengchiang000/js%E5%9F%BA%E6%9C%AC%E8%A7%80%E5%BF%B5-typeof-vs-instanceof-4dcb89e315df) 4. [instanceof](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof)