# **Explain how prototypal inheritance works** 所有的 JavaScript Object 都有一個`__proto__`屬性,那是對另一個 Object 的引用,這被稱為 Object 的「原型(prototype)」。當在一個 Object 上訪問一個屬性時 ,如果在該 Object 上找不到屬性,JavaScript Engine 就會查看該 Object 的`__proto__`,以及`__proto__`的`__proto__`,就這樣一直找下去,直到發現了在某個`__proto__`上的屬性,或是到達了原型鏈(prototype chain)的終點。 [這種行為模擬了繼承,但實際上更像是一種代理。](https://davidwalsh.name/javascript-objects) ![https://i.imgur.com/x6r0iU8.png](https://i.imgur.com/x6r0iU8.png) ## prototype chain 的終點:null。 null 在定義裡沒有原型,也是原型鏈(prototype chain)的最後一個鏈結。 ![https://i.imgur.com/FJQdMF7.png](https://i.imgur.com/FJQdMF7.png) ## **原型鏈(prototype chain)** 在進行實作前,先介紹原型鏈。 原型鏈是一條透過 `__proto__` 不斷串起來的鏈。 透過這一條原型鏈,就可以達成類似繼承的功能,可以呼叫自己 parent 的 method。 ![](https://imgconvert.csdnimg.cn/aHR0cDovL3Jlc291cmNlLm11eWl5LmNuL2ltYWdlLzIwMTktMDctMjQtMDYwMzE2LnBuZw?x-oss-process=image/format,png) ## **原型鏈最重要的兩個概念: `prototype` 跟 `constructor`** - 屬性 (`prototype`): - 每個 Object 都有的私有屬性(private property)。 - 它持有一個與其他物件(object)的鏈結(`__proto__`)。 - 建構式 (`constructor`): - Object 的每個 instance 都具有 `constructor` 屬性。 - `constructor`(建構式/建構子)保存的內容是「用來創建當前對象的 function」。 - `constructor` 是 function 特有的、在 function 的 prototype 上面的屬性,他會指向 function 引用的對象。 ![](https://i.imgur.com/a0x2NM9.png) ## **new 跟原型鏈的關係** 有了原型鏈的概念之後,就不難理解 new 這個關鍵字背後會做的事情是什麼。 假設現在有一行程式碼是:`let nick = new Person('nick');`,那它會做以下幾件事情: 1. 創出一個新的 object 2. 把 object 的 `__proto__` 指向 `Person` 的 `prototype`:這樣才能繼承原型鏈 3. 拿 object 當作 context(框架),呼叫 `Person` 這個建構函式建立內容 4. 回傳 object 給 nick 這個變數 ```jsx const Person = function (myName) { this.age = "40"; this.name = myName; }; let nick = new Person('nick'); ``` # **實例 (參考面試題庫範例,包含修正範例的錯誤)** 有一個 `Parent` 建構函式和一個`Child` 建構函式,要讓`Child` 做原型鏈繼承(繼承`Parent`)。 - 範例的解法(錯誤):使用`Object.create`的方式繼承。 ```jsx const Parent = function () { this.age = "40"; this.name = "Parent name"; }; Parent.prototype.greet = function () { console.log("hello from Parent"); }; // child 繼承 Parent const child = Object.create(Parent.prototype); //繼承 Parent child.cry = function () { //新增 child 自己的 function console.log("waaaaaahhhh!"); }; console.log(child); child.cry(); // Outputs: waaaaaahhhh! //成功呼叫 cry() child.greet(); // Outputs: hello from Parent //成功呼叫 Parent.prototype 建立的 greet() console.log(child.age); // Outputs: undefined //沒有繼承到 Parent 本身的 age 跟 name 屬性 //這時候 child.constructor 是指向 Parent (正確) console.log(child.constructor.name); //Parent ``` 因為只有`constructor`有繼承到`Parent`,但沒有繼承到 `Parent` 本身的 `age` 跟 `name` 屬性。 所以參考面試題庫範例的做法,透過修正 `constructor` 指向來串接物件上的建構子:使用 `Parent.call(this);` (如下)。 - 範例的解法(錯誤):使用`Object.create`+ `Parent.call(this);` 來串接物件上的建構子。 ```jsx const Parent = function () { this.age = "40"; this.name = "Parent name"; }; Parent.prototype.greet = function () { console.log("hello from Parent"); }; const child = Object.create(Parent.prototype); //繼承 Parent //修正 constructor 指向 console.log("----- 修正 constructor 指向 -----"); function Child_new() { //建立一個 Child_new 的建構函式 Parent.call(this); //使用 call 來串接物件上的建構子 this.name = "Child new name"; } // Child_new 繼承 Parent Child_new.prototype = Parent.prototype; //繼承 Parent Child_new.prototype.constructor = Child_new; //把 constructor 指向 Child_new /* 註:如果是使用 Child_new.prototype = Parent.prototype; 的寫法,當我們對 Child_new.prototype.constructor 重新賦值(Child_new)時,會一起改掉 Parent.prototype.constructor (從 Parent 改成 Child_new) */ const child_new = new Child_new(); //建立 Child_new 的 instance child_new.cry = function () { //新增 child_new 自己的 function console.log("waaaaaahhhh!"); }; console.log(child_new); child_new.cry(); // Outputs: waaaaaahhhh! //成功呼叫 cry() child_new.greet(); // Outputs: hello from Parent //成功呼叫 Parent.prototype 建立的 greet() console.log(child_new.age); // Outputs: 40 //成功繼承 Parent 本身的 age console.log(child_new.constructor.name); // Outputs: "Child_new" console.log(child.constructor.name); // Outputs: "Child_new"(錯誤) /* 註:可以發現繼承自Parent的child物件的constructer被改成"Child_new"了:也就是說 Parent.prototype.constructor 被改成當前的"Child_new"了 */ ``` 使用 `Child.prototype = Parent.prototype`會改掉 `Parent.prototype.constructor`的指向。 所以在做繼承時,應該要使用 `Child.prototype = Object.create(Parent.prototype);` 或 `Child2.prototype = new Parent();` 來避免修改到 `Parent` 的 `constructor` 。 ```jsx const Parent = function () { this.age = "40"; this.name = "Parent name"; }; Parent.prototype.greet = function () { console.log("hello from Parent"); }; const child = Object.create(Parent.prototype); //繼承 Parent //修改繼承的方式 console.log("----- Child 2 -----"); function Child2() { //建立一個 Child2 的建構函式 Parent.call(this); this.name = "Child new name"; } // Child2 繼承 Parent // Child2.prototype = Object.create(Parent.prototype); //第一種繼承的寫法 Child2.prototype = new Parent(); //第二種繼承的寫法 Child2.prototype.constructor = Child2; //把 constructor 指向 Child2 const child2 = new Child2(); //建立 Child2 的 instance child2.cry = function () { //新增 child2 自己的 function console.log("waaaaaahhhh!"); }; console.log(child2); child2.cry(); // Outputs: waaaaaahhhh! //成功呼叫 cry() child2.greet(); // Outputs: hello from Parent //成功呼叫 Parent.prototype 建立的 greet() console.log(child2.age); // Outputs: 40 //成功繼承 Parent 本身的 age console.log(child2.constructor.name); // Outputs: "Child2" console.log(child.constructor.name); // Outputs: "Parent"(正確) console.log("----- 測試 hasOwnProperty -----"); console.log(child2.hasOwnProperty("name")); // Outputs: true (是直接屬性,結果應該為 true) console.log(child2.hasOwnProperty("age")); // Outputs: true (是從原型鍊繼承的屬性,結果應該為 false)(錯誤) /* child2.hasOwnProperty("age") 為 true 是錯誤的,代表我們做的仍然不是正確的原型鏈繼承 */ ``` 最後查到原因應該是出在 `call`,刪掉 `Parent.call(this);` ```jsx const Parent = function () { this.age = "40"; this.name = "Parent name"; }; Parent.prototype.greet = function () { console.log("hello from Parent"); }; const child = Object.create(Parent.prototype); //繼承 Parent /* 刪掉 Parent.call(this); */ function Child3() { //建立一個 Child3 的建構函式 //刪掉原本在這裡的 Parent.call(this); this.name = "child3"; } // Child2 繼承 Parent Child3.prototype = new Parent(); //第二種繼承的寫法 Child3.prototype.constructor = Child3; //把 constructor 指向 Child3 const child3 = new Child3(); //建立 Child3 的 instance child3.cry = function () { //新增 child3 自己的 function console.log("waaaaaahhhh!"); }; console.log(child3); child3.cry(); // Outputs: waaaaaahhhh! //成功呼叫 cry() child3.greet(); // Outputs: hello from Parent //成功呼叫 Parent.prototype 建立的 greet() console.log(child3.age); // Outputs: 40 //成功繼承 Parent 本身的 age console.log(child3.constructor.name); // Outputs: "Child3" console.log(child.constructor.name); // Outputs: "Parent"(正確) console.log("----- 測試 hasOwnProperty -----"); console.log(child3.hasOwnProperty("name")); //true (直接屬性) console.log(child3.hasOwnProperty("age")); //false (從原型鍊繼承的屬性) ``` # 總結 當寫了一個新的建構函式(`Child3`),又想把他綁到原型鏈(`Parent`)上時,可以依照以下步驟: 1. 使用 `Child.prototype = Object.create(Parent.prototype);` 或 `Child2.prototype = new Parent();` 來繼承 `prototype` 2. 把 `constructor` 指向 新的建構函式 (`Child3`) ## 如何測試是否正確繼承原型鏈 可以使用 `hasOwnProperty` 來測試新的建構函式的屬性。 - 若是新的建構函式上具備的屬性,會回傳true (直接屬性); - 若是新的建構函式上不具備的屬性(且確認在原型鏈上有這個屬性),會回傳false (從原型鍊繼承的屬性) # **補充資料** ## **原型鏈的優缺點:** - 優點: 1. 這樣將父類的 instance 作為子類的原型,容易實現。 2. 父類後續新增的屬性或方法,子類中都可以訪問。 3. 減少不必要的記憶體消耗 4. 操作子類即可修改原型中的屬性。(同缺點 4) - 缺點: 1. 子類型的原型上的 constructor 屬性被重寫了 2. 給子類型原型添加屬性和方法必須在替換原型之後 3. 創建子類型實例時無法向父類型的構造函數傳參 4. 操作子類即可修改原型中的屬性。(同優點 4) ## **原型鏈的效能** 原型鏈上的屬性的查詢時間,可能會對效能有負面影響,對程式碼也因而產生明顯問題。另外,試圖尋找不存在的屬性,就一定會遍歷整個原型鏈。接著,在迭代物件屬性時,每個原型鏈的枚舉屬性都會抓出來。 - 要檢查物件本身有沒有指定的屬性、也不需要查找整個原型鏈時,你可以使用由 `Object.prototype` 繼承的 `hasOwnProperty`方法。(MDN) ## **Object.create 與 new 的不同** - `new` 配合建構函式使用,建立一個新物件。 1. 建立 instance 物件 child 2. 呼叫建構函式(Parent)初始化 child 成員變數。 3. 指定 instance 物件的原型為 Parent.prototype 物件。即 `child.__proto__`指向 Parent.prototype。 - `Object.create()`:大部分的新瀏覽器都支援這樣的做法(Not supported in IE8 and below.),但如果某些瀏覽器的版本真的舊到無法使用 `Object.create()`,就要自己寫 polyfill。 1. 建立空物件{} 2. 指定空物件{}的原型為 Object.create()的引數。 - 參考資料 - [javascript中Object.create與new的不同](https://www.itread01.com/content/1545340514.html) - [[筆記] 談談JavaScript中最單純的原型繼承(prototypal inheritance)─ Object.create](https://pjchender.blogspot.com/2016/06/javascriptprototypal-inheritance.html) ## Base object v.s. null (JS面試題) ### 題目:[All objects have prototypes.](https://github.com/lydiahallie/javascript-questions#14-all-object-have-prototypes) :::info Ans: false. Because the base object doesn't have prototype. ::: The base object is `Object.prototype`: > The `Object.prototype` is a property of the Object constructor. And it is also the end of a prototype chain. ### 驗證:使用 `Object.getPrototypeOf()` 查詢原型物件是否有 property 例如: - 空物件的原型物件是 `Object.prototype` - 對 `{}` 使用 `Object.getPrototypeOf()` 來查詢`{}`的原型物件 ![](https://i.imgur.com/axqubQ6.png) - 印出 `Object.prototype` 確認兩者是一樣的結果 ![](https://i.imgur.com/reLnDQn.png) - 展開印出的 `Object.prototype` ,確認**他是一個 Object** ![](https://i.imgur.com/XIu6CdQ.png) :::warning 但他沒有 [[prototype]] 屬性 ::: - 驗證 `Object.prototype` 沒有 [[prototype]] 屬性: ![](https://i.imgur.com/djdRj9M.png) ### 延伸問題:What is the end of prototype chain in javascript — null or Object.prototype? :::info Ans: null. ::: - 詳細討論: [What is the end of prototype chain in javascript -- null or Object.prototype?](https://stackoverflow.com/questions/36692927/what-is-the-end-of-prototype-chain-in-javascript-null-or-object-prototype) - **參考資料** - 影片類 - [https://www.udemy.com/course/javascript-es6-react-redux/learn/lecture/9088422#overview](https://www.udemy.com/course/javascript-es6-react-redux/learn/lecture/9088422#overview) - [https://www.youtube.com/watch?v=Tn4rfHyYRXU](https://www.youtube.com/watch?v=Tn4rfHyYRXU) - [https://www.youtube.com/watch?v=Ex9hhl6TdRE](https://www.youtube.com/watch?v=Ex9hhl6TdRE) - MDN - [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) - [https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Function/call](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Function/call) - [https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty) - 技術文章 - [https://blog.csdn.net/zengyonglan/article/details/53465505](https://blog.csdn.net/zengyonglan/article/details/53465505) - [https://www.cnblogs.com/tangjiao/p/10036485.html](https://www.cnblogs.com/tangjiao/p/10036485.html) - [https://blog.techbridge.cc/2017/04/22/javascript-prototype/](https://blog.techbridge.cc/2017/04/22/javascript-prototype/) - [https://blog.csdn.net/chern1992/article/details/106785509](https://blog.csdn.net/chern1992/article/details/106785509)