# JavaScript 的物件導向與原型繼承 ## 古典和原型繼承 繼承表示一個物件取用另一個物件屬性或方法,只要了解其簡單觀念就好了,許多人在解釋這區塊時常會用各種火車、汽車等等例子來做舉例,但講師認為直接講清楚會比較簡顯易懂。 那古典繼承和原型繼承是什麼呢?古典繼承在 C#、Java 裡都有,而且非常熱門。 而古典繼承裡面有非常多方法可以用 * friend * protected * private * interface 但我們必須了解他才能夠知道該如何操作。 原型繼承呢?東西就簡單許多了 * 彈性 (flexible) * 可擴充性 (extensible) * 簡單易懂 (east to understand) 古典和原型繼承各自都有他的好壞,所以並沒有一定,所以當有人在講繼承時,就是在講 > 一個物件取用另一個物件屬性或方法 ## 瞭解原型 ### 前言 JavaScript 用了原型繼承,所以代表有個叫作原型(prototype)的概念。 ### 瞭解原型 首先我們知道物件可以有屬性和方法,然後我們可以使用點運算子取得屬性或方法,JavaScript 中所有物件、函數,都有原型屬性,而這個屬性會參考到另一個屬性通常被稱為 proto。 假設今天我們有一個 obj 的物件,底下有一個叫 prop1,所以我們可以透過 obj.prop1 來取得。 ![](https://i.imgur.com/Gvbj9dX.png) JavaScript 中所有物件、函數,都有原型屬性。 ![](https://i.imgur.com/ujtvRes.png) 而這個屬性會參考到另一個物件,稱之為 proto,而 proto 也可以有屬性,例如 prop2,所以當我們要取用 prop2時就可以這樣寫 obj.prop2。 ![](https://i.imgur.com/Vq3wUOQ.png) 而我們使用點運算子去取用 obj 中的 prop2 時,會找不到,所以他會往原型裡面找。 ![](https://i.imgur.com/GGjQY6t.png) 那原型物件也可以指向到另一個物件。 ![](https://i.imgur.com/oW5l7hH.png) 每個物件都可以有自己的原型,當這個 proto 裡面有一個 prop3,我們就可以用 obj.prop3 取得。 ![](https://i.imgur.com/luirq2S.png) 而這過程就像一個鏈子,所以又稱為原型鏈 (prototype chain),但是不要把他跟範圍鏈搞混了,雖然很相似,可是範圍鏈是尋找可以取用的變數,但原型鏈是尋找屬性及方法。 ![](https://i.imgur.com/jLE3x65.png) 而一般來講這 proto 是隱藏起來的,所以我們才不用這樣撰寫 obj.proto.proto.prop3,只需要 obj.prop3 就好。 但是 JavaScript 中有一個很有趣的狀況,當若有第二個 obj 時,他可以指向同一個原型。 ![](https://i.imgur.com/wonD10e.png) 所以當我們呼叫 obj2.prop2,他一樣會回傳相同位子。 ![](https://i.imgur.com/Nj43kat.png) 而以上這些就是原型及原型鏈的概念,只需要想簡單一點只是有一個特別的參考到我們的物件而已,那接下來我們直接來看點範例。 ```javascript= var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', } ``` 這邊我們有兩個物件,接下來我們要將 john 設定成原型,但以下範例千萬不要使用於現實中,這只是為了簡單理解觀念而已。 ```javascript= var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 john.__proto__ = person; console.log(john.getFullName()); ``` 為什麼不是抓到 Default?因為原型鏈的原因導致,所以點運算子會在 john 裡面找到 firstname,所以就會停下來不會再找了,接下來在加一點物件上去。 ```javascript= var person = { firstname: 'Default', lastname: 'Default', getFullName: function() { return this.firstname + ' ' + this.lastname; } } var john = { firstname: 'John', lastname: 'Doe', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 john.__proto__ = person; console.log(john.getFullName()); var jane = { firstname: 'jane', } // 千萬不要使用這種方式在真正的專案開發上,這只是為了理解原型而已。 jane.__proto__ = person; console.log(jane.lastname); console.log(jane.getFullName()); ``` ## 所有東西都是物件(或純值) ### 前言 現在我們知道物件原型,接著我們可以深入瞭解到一件事情 JavaScript 所有東西都是物件或是純值 數值、布林、字串、函數、陣列、物件他們都有原型,除了基本物件(base object)。 ### 所有東西都是物件(或純值) 讓我們從範例來瞭解,接下將會利用這三個東西 物件、函數、陣列來講解為什麼所有東西都有原型。 ```javascript= var a = { }; var b = function() { }; var c = []; ``` 首先讓我們試著在瀏覽器輸入以上範例,然後再輸入 a.__proto__ ![](https://i.imgur.com/QLftT5u.png) 我們會得到一個基本物件,這在原型鏈上非常底層,而這基本物件有屬性與方法。 ![](https://i.imgur.com/Mg1ZB3g.png) 那函數呢?我們試著輸入 b.__proto__ ![](https://i.imgur.com/pmQBFdh.png) 這就是所有函數的原型,所有我們建立的函數都有這個原型,當然也有相關的屬性與方法(你會看到熟悉的 apply、call、bind)。 ![](https://i.imgur.com/iMaMKfa.png) 接下來是陣列 c.__proto__ ![](https://i.imgur.com/sBamYwS.png) 這就是一個原型陣列,我們也來看看它是否也有屬性與方法(這裡你也會看到許多熟悉的字眼)。 ![](https://i.imgur.com/75EshqI.png) 所以由上面這三個範例我們可以知道一件事情。 > JavaScript 所有物件、所有陣列及所有函數都有原型。 那這邊再講一個好玩的問題,原型的原型是什麼? ![](https://i.imgur.com/ul0CHwO.png) 是原型,所以我們要記得,原型鏈最底下的東西就是原型物件。 ## Reflection 與 Extend ### 前言 接下來將要講解另一個建立物件的函數很有趣很有效的東西,許多資料庫都會使用到,而這稱為 extend。 而通常我們都是用一個叫 Reflection 的東西來做到 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]); } ``` 這時候會看到一個很神奇的東西,getFullName(),這是因為 for in 會到外面取用屬性和方法的 不只在物件本身,還會到原型上找 那如果今天我們要 ```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) { if(john.hasOwnProperty(prop)) { // hasOwnProperty 是原型物件的東西,我們可以透過這種方式來確定是否物件有這個屬性。 console.log(prop + ':' + john[prop]); } } ``` 這樣 getFullName() 就沒有出現了,因為 john 的物件屬性並沒有 getFullName()。 而這個概念可以用來幫助我們做一些事情【補足原型繼承】,所以這邊試試看引入 underscore.js。 ```javascript= var newscript = document.createElement('script'); newscript.setAttribute('type','text/javascript'); newscript.setAttribute('src','https://underscorejs.org/underscore-min.js'); var john = { firstname: 'John', lastname: 'Doe', } var jane = { address: '111 Main St.', getFormFullName: function() { return this.lastname + ', ' + this.firstname; } } var jim = { getFirstName: function() { return firstname; } } _.extend(john, jane, jim); // 這樣做將會把物件結合起來 console.log(john); ``` 這時候我們可以看到 john 有 jane 的物件地址、jim 物件的 getFirstName 函數,還有 jane 物件的 getFormalFullName()。 那這是怎麼做到的?首先先開啟 underscore.js,開啟之前記得千萬不要害怕開源的專案,這是一個非常好學習教材,我們試著找 extend 看看。 ![](https://i.imgur.com/laTPkTh.png) 首先我們可以看到他建立了屬性或方法,而這個叫 createAssigner 的函數我們試著找看看。 ![](https://i.imgur.com/mQfQs3G.png) 我們可以發現 createAssigner 是一個函數,其中他接受了 keys 還有 defaults,然後回傳函數本身,我們可以看到這是一個閉包。 首先它會先取得傳入參數的長度 ![](https://i.imgur.com/MEJRs5x.png) 再來是包裝物件,他將物件包裝起來(避免參考問題) (((我猜))) ![](https://i.imgur.com/TpSB3A7.png) 接下來就是當物件長度若小於兩筆,或是物件為空,就返回物件 ![](https://i.imgur.com/5gonJh7.png) 接下來他會用 for 將所有物件跑一遍,因為陣列第一筆是 john ,所以起始會直接跳過 0 從 1 開始 ![](https://i.imgur.com/oJfcSJq.png) 所以因為經過 underscore.js 的處理, jim 就可以使用 getFormFullName()了。 ```javascript= var newscript = document.createElement('script'); newscript.setAttribute('type','text/javascript'); newscript.setAttribute('src','https://underscorejs.org/underscore-min.js'); var john = { firstname: 'John', lastname: 'Doe', } var jane = { address: '111 Main St.', getFormFullName: function() { return this.lastname + ', ' + this.firstname; } } var jim = { getFirstName: function() { return firstname; } } _.extend(john, jane, jim); // 這樣做將會把物件結合起來 console.log(john); john.getFormFullName(); ``` ![](https://i.imgur.com/NlhWZUm.png) ###### tags: `WierdJavascript`