# The Weird Part Of Javascript - part 4 ###### tags: `Javascript` # Section 5 - Object-Oriented Javascript And Prototypal Inheritance # Conceptual Aside: Classical vs Prototypal Inheritance ## Inheritance > 一個物件可以取得 屬性以及方法 從另一個物件上面 ## Classical Inheritance 是一種分享物件 屬性以及方法的一種方式 * 缺點是非常攏長 Verbose * 其中的交錯非常複雜難以得知其中的物件如何影響其他的物件 作者舉的例子 就像是 你在裝潢家裡可是當你更換燈泡時,你的馬桶卻沖水了 * 有許多 API 需要使用 friend protected private interface ## Prototypal Inheritance 作者介紹的優點: Simple flexible extensible easy to understand # Understanding The Prototype > 在 JS 裡面每一個物件都有 prototype 屬性 ## Prototype Chain(原型鍊) ![](https://i.imgur.com/M0NtDcl.png) 在 JS 裡面每一個物件都有 prototype 屬性,簡單來說就是一個其他物件的參考點也就是 proto 圖片的範例中 obj 想要使用 prop2 因為在 obj 內找不到,這時候就會透過 proto 這個參考點去尋找其他物件的屬性,直到找到 prop2, prop3 以此類推,**而這個一直往下找的過程就是 Prototype Chain** 所以可以理解到的是我們在尋找的 prop1 2 3 ,都不是在 obj 內找到的,而是透過 prototype 的那個 proto 物件,並且返回尋找的內容 然而我們如果要得到 prop3 的值,我們不需要寫成 `obj.proto.proto.prop3` 而是直接寫成 `obj.prop3` JS 引擎會在背後幫我們搜尋 原型鍊 ## 其他物件也想取得同樣的屬性以及方法 其他的物件想要取得 prop2 可以辦得到嗎? 可以,只要使用一樣的 proto 即可以取得一樣的物件屬性以及方法 ![](https://i.imgur.com/MUml9J0.png) ## 程式碼範例 這邊為了展示 prototype 的繼承,直接使用了 __proto__ ,現實中因應效能問題請勿使用 ```javascript= var person = { firstname:'Default', lastname:'Default', getFullName:function(){ return this.firstname + ' ' + this.lastname; } } var john = { firstname:'John', lastname:'Doe' } // 別使用這邊的方法,只限 DEMO 使用 john.__proto__ = person; // 這時候 john 就會繼承 person,如果這時候我想要使用 john本身沒有的屬性或是方法時,JS 引擎就會往 person 去找 console.log(john.getFullName()); // 就會引用 person 的 function 印出 John Doe ``` 接下來有個問題 為何這邊 firstname 印出來是 John ,很簡單因為在 John 本身的物件他就找到此屬性了,因此就直接返回而不會再往下搜尋原型鍊 ```javascript= john.__proto__ = person; console.log(john.firstname); // John ``` 那個這樣的狀態呢? 因為在 jane 物件中找不到 lastname 這個屬性所以透過原型鍊的幫助抓到 person 的 Default ```javascript= var jane = { firstname = 'Jane' } jane.__proto__ = person; console.log(jane.lastname); // Default ``` # Everything is an Object (or a Primitive) > 這章主要利用 `__proto__` 來找到原型鍊的最底層, 會發現原來大家都一樣 ```javascript= var a = {}; var b = function () {}; var c = []; ``` a 為空的物件 b 為 function c 為 array * a 已經為最底層的物件 * b 使用 `__proto__` 兩次達到底層 * c 使用 `__proto__` 兩次達到底層 * 發現得到可以用的函式都是一樣的 ![](https://i.imgur.com/GLSPeet.png) > 從這裡可以證明所有的 物件 函式 陣列 都是物件,其他就是純值(primitive) # Reflection and Extend ## Reflection > 物件可以看著它自己並且可以監聽以及改變自己的屬性以及方法 ```javascript= var person = { firstname:'Default', lastname:'Default', getFullName:function(){ return this.firstname + ' ' + this.lastname; } } var john = { firstname:'John', lastname:'Doe' } john.__proto__ = person; for(var prop in john){ console.log(prop+':'+john[prop]); } ``` 這邊印出的結果會是所有 john 內部目前的內容也就包含了 john 沒有的方法也就是 getFullName,不過它因為 `__proto__` 的關係也會從 person 去拿 ![](https://i.imgur.com/mOSRt7k.png) 但如果我只想要方法或是屬性只存在我要物件中呢? 這邊可以使用 hasOwnProperty 是個 object 內建的方法,可以辨別是否物件有此屬性有的話返回 true 沒有則 false 來執行這個判斷式 ```javascript= for (var prop in john) { if (john.hasOwnProperty(prop)) { console.log(prop + ': ' + john[prop]); } } ``` 印出結果 可以看出就篩選掉了不是 john 的方法瞜 ![](https://i.imgur.com/WVCWdQy.png) 寫到這邊只會 `console.log()` 出真正有出現在 john 內的屬性或是方法 也就是 firstname, lastname ,這兩個屬性是真正存在 john 物件內部的 運用這樣的方式可以 reflect john 這個物件本身觀察它自己的屬性,以及是否是真的存在於 john 本身(像是 getFullName 就是不是),並且可以改變那些屬性甚至操作他們。 但是這樣的方法並不是 build-in 在 JS 內,所以很多的框架或是函式庫會寫它們自己的 reflection ,這邊會用 underscore.js 函式庫來作範例 ## underscore 範例 在使用 underscore 的範例中,可以看出 john 直接繼承了 jane, jim 的內容,並且有延續 person 的 prototype ```javascript= var john = { firstname:'John', lastname:'Doe' } var jane ={ address:'111 main street', getFullName: function (){ return this.firstname+' '+ this.lastname } } var jim ={ getFirstName: function (){ return this.firstName; } } _.extend(john,jane,jim) console.log(john); // Object { // firstname:'john', // lastname: 'Doe', // address:'111 main street', // getFirstName: function, // getFullName: function // } ``` 在 underscore 的 source code 只要是由這個 createAssigner 函式來處理 var length 的部分是 argument 的長度以上面的例子來看的話 1. `if (defaults) obj = Object(obj);` 這部分則是代表如果參數數量小於2 或是 obj 為空則直接返回物件本身,簡單來說就是沒有放入要被繼承的參數因此直接返回原本的物件 1. 進入第一個迴圈這邊印出除了 john 以外的其他參數(因為它index 使用從 1 開始) 1. 並在其中的第二個環圈印出其他參數的屬性使用 keysFunc 1. l = key.length 則幫助第二個迴圈印出所有的屬性 1. 並且第二個迴圈會判斷是否 屬性為空 如果不為空則把其他屬性指派給 john 1. 最後返回物件 ```javascript= function createAssigner(keysFunc, defaults) { return function(obj) { var length = arguments.length; if (defaults) obj = Object(obj); if (length < 2 || obj == null) return obj; for (var index = 1; index < length; index++) { var source = arguments[index], keys = keysFunc(source), l = keys.length; for (var i = 0; i < l; i++) { var key = keys[i]; if (!defaults || obj[key] === void 0) obj[key] = source[key]; } } return obj; }; } ``` # Section 6: Building Objects ## new key word 以及 function constructor 雖然有更新的方式可以創造物件,但這邊我們要先了解這兩個創造物件的方法,畢竟他們也流傳很廣,並且使用正確的方式設定 prototype ```javascript= function Person() { console.log(this); this.firstname = 'john'; this.lastname = 'doe'; console.log('This function is invoked'); } var john = new Person(); console.log(john); // Person{firstname:'john', lastname:'doe'} ``` 然而,其實這個 new key word 是個 operator ![](https://i.imgur.com/lhSYxA2.png) 當 new 出現的時候,一個空的物件出現並且他後面呼叫了 Person 函式,並且藉著 Person 函式把 firstname/lastname 加進去那個空物件中(this 指向空物件),並且 Person 函式並沒有 return value ,因此就返回了空的物件(但現在有被加進內容了) 並且要證明這個函式有被呼叫的方法很簡單就是 log 出內容即可 ![](https://i.imgur.com/dQ38uTA.png) 那我們印出最開始的 this 是什麼(未操作前)會發現其實就是空的物件 ![](https://i.imgur.com/6NPuEw7.png) 那如果本來的 Person function 有返回值則擋住擋住上面 this 指派給 new 空物件的內容直接替換成返回值的內容 ```javascript= function Person() { console.log(this); this.firstname = 'john'; this.lastname = 'doe'; console.log('This function is invoked'); return { greeting :'i got in the way'}; } var john = new Person(); console.log(john); // Person{firstname:'john', lastname:'doe'} ``` 可以看到 return 內容直接取代上面 this 的操作 ![](https://i.imgur.com/85c8Krf.png) 如果我們在創建一個 jane 會生什麼事呢? ```javascript= var jane = new Person(); ``` 它就會在產出一次一樣的內容 ![](https://i.imgur.com/GEpABrl.png) 所以如果我想要透過創建人物的時候可以操作其姓名怎麼處理呢? 帶入參數 ```javascript= function Person(firstname,lastname) { console.log(this); this.firstname = firstname; this.lastname = lastname; console.log('This function is invoked'); return { greeting :'i got in the way'}; } var john = new Person('john', 'doe') console.log(john) var jane = new Person('jane', 'doe') console.log(jane) ``` 印出結果 就是我們帶進去的參數摟 ![](https://i.imgur.com/jIXd3nQ.png) ## Big word alert: Function Constructors > 就是一個普通的 function 不過他的功能是用來創建物件的內容為其加入屬性或是方法 也就是我們剛剛看到的 `Person()` 這個就是 function constructor ,會來搭建前面 new 出來的空物件的內容藉由當 excution context 的 creation phase 時都會創造的 this 變數來達成 ```javascript= var john = new Person() ``` 並且因為那個 function constructor 並沒有返回值所以會返回那個被設置好的空物件,當 function constructor 執行結束時 # Function Constructors and '.prototype' 當你使用 function constructors 時,它就會自動設定好 prototype 給你 每個 function 都有 prototype 這個屬性可是只有當它是使用 new operator 的這個 function constructors 創造時才會啟用它(會以一個空物件的形式被創建) 然而 `__proto__` 這個屬性則是每個物件都有,他們兩個是不相同的! ![](https://i.imgur.com/wfg2bGr.png) 所以我使用它的方式如下: 直接對 Person 使用 .prototype 並且添加上去需要的方法或是屬性,就可以直接套用到 john 或是 jane 上面非常的方便,此時 Person.prototype 就是他們的 prototype ```javascript= function Person(firstname,lastname) { console.log(this); this.firstname = firstname; this.lastname = lastname; console.log('This function is invoked'); return { greeting :'i got in the way'}; } Person.prototype.getFullName = functoin(){ return this.firstname+ ' ' + this.lastname; } var john = new Person('john', 'doe') console.log(john) // 印出的 john 會包含 getFullname 方法 var jane = new Person('jane', 'doe') console.log(jane) // 印出的 jane 會包含 getFullname 方法 ``` 甚至可以在創造好 john, jane 之後再添加方法或是屬性上去都是可以的! ```javascript= Person.prototype.getFormalFullName =function() { return this.lastname + ', ' + this.firstname; } console.log(john.getFormalFullName()); ``` 這邊就會直接印出剛剛新增上去的方法 Doe, John 從這邊可以理解到所有我使用 function constructors 創造的物件,我都可以在創造之後的任意時間點新增方法上去藉著使用 .prototype 這個屬性 所以如果我創造了 1000 個人,如果我把方法寫在 Person 裏面,那麼那行重複的程式碼就會重複被創造 1000 遍一樣的程式碼非常佔空間 因此可以藉由 .prototype 給他們增加方法只要使用這行程式碼即可,可以節省很多記憶體空間 這就是你使用 function constructors 來使用 .portotype 的方式 # Dangerous Aside: 'new' and functions 危險的地方是如果在使用 function constructor 時忘記加上 new 關鍵字 因為 new 關鍵字是 Person function 返回的值被拿走了,所以只會得出 undefined ```javascript= var john = Person('john', 'doe') console.log(john) // undefined Person.prototype.getFormalFullName =function() { return this.lastname + ', ' + this.firstname; } console.log(john.getFormalFullName());// 會直接報錯 ``` 所以我們用來使用當作 function constructor 的 function 會以大寫字母開頭命名比方說 `Person()` 當我們看到報錯時並且有大寫的函式名稱可能就是忘記加上 new 關鍵字摟 雖然有新的創造物件的方法出現,不過要讀懂前人的扣這部分還是必須理解 # Conceptual Aside: Built-In Function Constructors 這邊會介紹一些內建在 JS 引擎內部的 function constructor ```javascript= var a = new Number(3); console.log(a) ``` 我們操作在瀏覽器裡的 console 會得到 a 不是 number 而是物件,因為 function constructor 創造的是物件! ![](https://i.imgur.com/b0jXeNT.png) 既然創造出來的 a 是由 function constructor 創造的物件那麼它自然就會有 prototype 屬性可以使用 ![](https://i.imgur.com/w354Dxl.png) 並且可以發現有很多已經被寫好可以使用的方法在裡面 所以我可以操作我剛剛設置好的 a 使用 tofixed 方法得出他的小數點後兩位 ```javascript= a.toFixed(2) // "3.00" ``` --- 這邊我們來試試看不同的方法 ```javascript= var a = new String('john') console.log(a) ``` 這邊一樣會 log 出來的內容是物件 ![](https://i.imgur.com/WeHuEz7.png) 並且 a 也有 prototype 設置好的方法可以使用(因為它指向 String ),這些方法是附加在 String 這個 function constructor 內 ![](https://i.imgur.com/jbQCvp8.png) --- 有時候 JS 引擎可以直接理解字串的輸入(儘管不是使用 function constructor) 把它包進去 String 物件裡面因此它可以正常使用 prototype 屬性的方法 ```javascript= "john".length() ``` 這兩個是一樣的 ```javascript= new String('john').length ``` ![](https://i.imgur.com/v76xRUZ.png) 使用 number 的話則會報錯摟,他也不能使用新增的方法必須乖乖使用 function constructor 才能 ![](https://i.imgur.com/oPhEDik.png) 這邊我們嘗試對所有的 String 物件新增方法 ```javascript= String.prototype.isLengthGreaterThan = function(limit){ return this.length > limit } console.log("john".isLegnthGreaterThan(3)) // true ``` 這邊發生了什麼事呢? 這邊的 "john" 自動地被辨別成 String 物件,並且我在上方給所有的 String 新增了方法 isLengthGreaterThan,因此所有的 String 都可以使用,因此 "john" 也可以使用 很多的函式庫以及框架也都利用這樣的方法來新增新的功能,不過使用這個方法的時候要注意不要複寫其他已經在裡面的功能