# Q1 - Prototype Chain and Class Inheritance <br> Outline : - 其他語言的繼承( Classical inheritance ) - JS 的繼承 ( Prototypal inheritance ) - 原型鏈(Prototype Chain) - 使用原型繼承的好處 - ES6 中的 class 語法糖 ## 前言:簡單了解「其他語言」的繼承 所謂的繼承,簡要而言,就是**一個物件可以將自己的屬性(property)或方法(method),提供給其他物件使用 ; 或者反過來說,一個物件的屬性(property)或方法(method)可以被其他物件提取**。 而提供屬性、方法給其他物件使用的原始物件,可以視為模組,也就是 class。 ### Class (類別) 、Instance(實體)與 Classical inheritance Class (類別) -> 規格書、模組的概念 以製作狗狗為例子,假設想要做各種類型的狗狗,這些狗狗有各自的毛色和體型,但全部都會叫,這時候,最原始的模組 class,可能會長這樣。 ![](https://i.imgur.com/tvkhoLx.png) - 所有的狗都會執行叫這個動作的 func,這類「動態操作」稱之為「方法」。 - 每隻狗有不同的顏色和體型,這類「靜態屬性」稱之為「屬性」。 有了模組 class 後,搭配 new 命令式語法,能做出多種的 instance(實體對象)。 ![](https://i.imgur.com/CwkRWmt.png) ( Java 程式。) ![](https://i.imgur.com/utUz55H.png) 所以很多隻的狗狗物件有自己的顏色和體型,也都擁有同樣從 class 中提取的叫方法,這樣就是種繼承。 這類型的繼承,就稱為 **classical inheritance**。 <br> ## 從 JS 緣起了解 JS 的原型繼承 Brendan Eich 最初在設計 JavaScript 時,數據類型都是對象 object (參考當時火紅的 object-oriented programming 語言),例如: Java。 But!他並沒有設計 class 的概念,可能是因當時只是想解決一些相對 server 而言不複雜的 client 端問題,不想太複雜或讓語言太困難。 然而還是必須要有一種機制,將所有 object 聯繫起來運作,因此最後還是設計了「 繼承 」的概念。 於是參考其他語言後,打算引入 new 命令式來從一個「原型對象(模型)」生成一個「實體對象 (intance)」。 But ! JS 中沒有「 class 」的設計,哪來原型對象? 因此做一個簡化的設計,由於 Java / C++ 等,在使用 new 時,都會調用 class 中的建構函式(constructor)。所以就 **利用 constructor 作為模型對象去 new 出 instance**。 ```java= // 「 Java 」 中, class 中有 constructor // 在此其餘語法不是重點,注意第 7 行與第 21 行即可 class Human { String name; int age; int height; Human(String str,int a,int b){ // constructor 建構函式初始化用 name=str; age=a; height=b; } void eat(){ System.out.println("eating"); } } //end of class Human class Test{ public static void main(String[] args){ Human h1 = new Human("小木",22,178); // new 時使用 constructor } } // end of class Test ``` ```javascript= // 「 JS 」中函式可以做為 constructor 使用 // 利用 constructor 生成 instance function DOG(name){ this.name = name this.species = '犬' } const dogA = new DOG('肚子') const dogB = new DOG('吐司') console.log(dogA.name) // 肚子 console.log(dogB.name) // 吐司 console.log(dogA) // DOG {name: "肚子", species: "犬"} console.log(dogB) // DOG {name: "吐司", species: "犬"} // 無法共享「屬性(Property)」和「方法(Method)」 dogA.species = '貓' console.log(dogB.species) // 犬 -> 不會受到 dogA 影響,無法共享屬性 ``` But ! 用 constructor 生成 instance 是無法共享「屬性」和「方法」。 如此一來,雖然能達成非共享內容的繼承,卻沒辦法達成共享繼承,會導致一些麻煩,例如:記憶體問題、程式撰寫不方便等。 因此 Brendan Eich 決定設計一個 **prototype 屬性**,這個屬性會屬於一個對象(constructor),而所有由這個對象創建的 instance 需要共享 prototype 中的屬性和方法。 ```javascript= // constructor function DOG(name){ this.name = name; } // constructor 中的 prototype DOG.prototype = { species : '犬' } // 生成 instance : dogA & dogB const dogA = new DOG('肚子') const dogB = new DOG('吐司') console.log(dogA) // DOG {name: "肚子"} console.log(dogB) // DOG {name: "吐司"} console.log(dogA.species) // 犬 console.log(dogB.species) // 犬 ``` - **需要共享**的屬性和方法,就放在 prototype (constructor.prototype)裡,prototype 是物件。 - **不需要共享**的屬性和方法,就放在 constructor 裡,constructor 是物件。 - instance 一創建,會自動引入 prototype 物件中的屬性和方法。 這正是所謂的「**原型繼承 prototypal inheritance**」,prototype 的對象 (DOG) 就像是 instance (dogA / dogB) 的原型,而 instance (dogA / dogB) 像是繼承 prototype 對象 (DOG) 一樣。 <br> ## 所以,DOG.prototype,存在於 instance 的哪裡? 把 instance (dogA / dogB) 印出來看看!找找 DOG.prototype 物件在哪。 ![](https://i.imgur.com/ZIoPZ8j.png) 會發現在 dogA / dogB 中,都有一個特別的 **_proto_ 物件**,這個物件中,就存在來自父層級繼承來的 prototype 。 更精確地說法是:**_proto_ 會在每個物件被生成時,自動被指派到該物件的屬性上,並會指向該物件的原型物件的 prototype**。 依照上例而言,dogA 物件在被 new 出來時,就有帶有 `_proto_` 屬性,並由於 `DOG.prototype = { species : '犬' }` ,所以 dogA._proto_ 就是 `{ species : '犬' }` 。 ```javascript= // 當我們使用 dogA.species 時,會先在自己的物件內尋找,發現只有 name 沒有 species 屬性 ,於是會再自動去尋找 dogA._proto_,發現有 species 屬性於是停止。 console.log(dogA.species) // 犬 console.log(dogA._proto_.species) // 犬 ``` ![](https://i.imgur.com/2vaHkI3.png) ```javascript= DOG.prototype === dogA._proto_ //true ``` ## 小結 1. JavaScript 裡沒有 class 模型,是透過 new + constructor 建立 instance 們。 2. instance 們可以透過 constructor.prototype 的方式,共同繼承 prototype 物件中的屬性和方法。 3. _proto_ 存在於每個物件中,指向原型物件的 prototype。當呼叫物件的屬性或方法時,會先尋找物件本身有沒有,沒有的話就會再往物件的 _proto_ 去尋找。 --- ## 繼承(Inheritance) 一個物件可以提取到其他物件中的屬性(property)或方法(method)。 繼承可以分成兩種,一種是 classical inheritance,這種方式用在 C# 或 JAVA 當中;另一種則是 JavaScript 所使用的,是屬於 prototypal inheritance。 ## 原型鏈(Prototype Chain) Javascript 使用的是 prototypal inheritance,所以必然會用到原型(prototype)的概念。 - prototype:會一直存在於建構函式(constructor)上的屬性,所有透過該函式產生的物件都有能力存取。 - _proto_:會在物件被生成時一起被指派到物件上的屬性,他決定這個物件的原型物件是誰。 --- ## 如果一直往父層的父層的父層的...找下去? ```javascript= function Person(name, age) { this.name = name; this.age = age; } Person.prototype.log = function () { console.log(this.name + ', age:' + this.age); } var nick = new Person('nick', 18); ``` **最後都會找到原型鏈最底端的`Object`(原型物件)。** ![](https://i.imgur.com/qGnVURQ.png) --- Javascript 的純值、物件都有原型鏈,並透過原型鏈繼承屬性、方法: ```javascript= var a = {}; var b = function() {}; var c = []; ``` ![](https://i.imgur.com/6wf5233.png) --- 如果需要的話,不同物件可以分享一樣的原型,它們共享同一個屬性但不是直接的,藉由原型鏈的概念,當 Javascript 搜尋 prop2 時會指向同一個位置。 ![原型繼承](https://i.imgur.com/v1KtfeH.png =x300) 舉個例子: ```javascript= var person = { firstname: "Default", // 屬性 lastname: "Default", // 屬性 getFullName: function() { // 方法 return this.firstname + " " + this.lastname; } }; var john = { firstname: "John", lastname: "Doe" }; // 將 john proto 物件指向 person john.__proto__ = person; // john 繼承自 person // john 裡面找不到所以到 person 找 console.log(john.getFullName()); // John Doe // 在 john 中找得到的屬性就不會到原型鏈上找 console.log(john.firstname); // John ``` 圖示: ![](https://i.imgur.com/jDFqpph.png =x400) --- ## 為何要將屬性和方法建立在原型物件上? ```javascript= function Person(firstname, lastname) { this.firstname = firstname; this.lastname = lastname; } // 新增屬性和方法在 Person 的原型上 Person.prototype.getFullName = function() { return this.firstname + " " + this.lastname; }; var john = new Person("John", "Doe"); console.log(john.getFullName()); // John Doe ``` 如果我們將所有的屬性和方法建立在函數中 `Person()`,倘若我們有 1000 個物件都透過 `new` 來建立時,就會有 1000 筆 `getFullName` 存在於記憶體當中,相當佔位置;但若建立在原型物件上,使物件在取用時透過原型鏈找到這屬性和方法,那麼就只需要存在記憶體中一次而不需要在每個物件裡面都建立。 --- ## 小結 <!-- ![Prototype chain](https://i.imgur.com/BaYL3qk.png) --> ![Prototype chain](https://i.imgur.com/2YhWlMC.png =x400) * `prop1`:`obj` 本身就**有** `prop1` 這個屬性,直接取用。 * `prop2`:`obj` 本身**沒有** `prop2`,當找不到時會往該物件的 `_proto_` 裡面尋找。 * `prop3`:同理找不到時會往 `_proto_` 這個物件裡面的 `_proto_` 再找下去。 `prop3` 實際是透過 `obj > _proto_ > _proto_` 這樣往下找的,你可以 `obj._proto_._proto_.prop3` 這樣得到 `prop3`,不過其實只要 `obj.prop3` 即可,而這樣尋找下去的鏈即為原型鏈(prototype chain)。 ![demo](https://i.imgur.com/v0L0rn8.png =x200) > 雖然利用原型繼承相當方便且強大,但必須注意程式碼內原型鏈的長度、必要時打破它們,以避免潛在的效能問題。再來,除非要處理 JavaScript 新語法的相容性,否則絕對不能擴充原生的原型。 --- ### JavaScript 中的(偽)Class Inheritance 由於 class 的用法在許多程式語言中都相當普遍,在 ES6 中也會添加了 Class 的用法,然而這種用法實際上和 classical inheritance 仍然是不同的!實際上只是 **syntax sugar**,在 JavaScript 中實踐繼承的方式依然是 prototypal inheritance 的方法。 class 的這種做法在 JavaScript ,只是用來建立物件和原型的另一種**更便利的撰寫方式**,但背後運作的邏輯其實和 function constructor 或者是 Object.create 都還是一樣的。 **可以視為:改用一種 class 的寫法實踐 Prototypal inheritance,這種寫法是抄 Class Inheritance 的寫法。** ```javascript= ///// class 語法糖寫法 ///// // 模組與創建 instance class Person{ // 建構函式 constructor(name){ // 私有 this.name = name; } // 方法 getName(){ return "Hello " + this.name ; // 共有 } } // 建構 john instance const john = new Person("John"); console.log(john); // Person { name: "John" } console.log(john._proto_);// {constructor: ƒ, getName: ƒ} ``` ```javascript= ///// prototype 寫法 ///// function Person(name) { // 私有 this.name = name; } Person.prototype = { getName: function(){ // 共有 return "Hello " + this.name } } // 建構 john instance const john = new Person("John"); console.log(john); // Person { name: "John" } console.log(john._proto_);// {constructor: ƒ, getName: ƒ} ``` 好處是「模組間」的繼承很好寫也好理解: ```javascript= // code from MDN class Animal{ constructor(name){ this.name = name; } speak(){ return this.name + ' makes a bark'; } sleap(){ return this.name + ' makes a sleep'; } } class Dog extends Animal{ speak(){ return this.name + ' makes a new bark'; } } const dogA = new Dog("肚子"); console.log(dogA); dogA.speak(); // 肚子 makes a new bark dogA.sleap(); // 肚子 makes a sleep ``` ## 總結 - JavaScrpit 的繼承是藉由 constructor 與 constructor.prototype 實踐 - 原形鏈是物件使用方法或屬性時,會先尋找自身是否有,再尋找 _proto_ 物件是否有,一層一層找上去的過程。 - ES6 的 class 是語法糖 --- 參考資料: - [該來理解 JavaScript 的原型鍊了](https://blog.techbridge.cc/2017/04/22/javascript-prototype/) - [Javascript继承机制的设计思想](http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html) - [什么是语法糖?](https://www.zhihu.com/question/20651624) - [Prototype chain-1](https://blog.techbridge.cc/2017/04/22/javascript-prototype/) - [Prototype chain-2](https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/prototype.html) - [Prototype chain-3](https://pjchender.blogspot.com/2016/06/javascriptprototypeprototype.html) - [class inheritance](https://pjchender.blogspot.com/2016/07/javascript-es6classes.html) - [[筆記] 談談 JavaScript 中的 function constructor 和 prototype 的建立](https://pjchender.blogspot.com/2016/06/javascriptfunction-constructorprototype.html) - [ [筆記] 談談 JavaScript 中的 function constructor 和關鍵字 new](https://pjchender.blogspot.com/2016/06/javascriptfunction-constructornew.html) - [[筆記] 談談JavaScript ES6中的Classes](https://pjchender.blogspot.com/2016/07/javascript-es6classes.html) - [初學者應知道的物件導向 JavaScript](https://developer.mozilla.org/zh-TW/docs/Learn/JavaScript/Objects/Object-oriented_JS) - [Java 備用筆記](https://yubin551.gitbooks.io/java-note/objectConstructor.html) **報告用投影片** 第一張投影片 ![](https://i.imgur.com/hfs5Lmq.png) 第二張投影片 ![](https://i.imgur.com/UpQ0sol.png)