--- tags: JavaScript title: 為什麼 JavaScript 會有那麼多內建語法可以使用? - 原型鏈 --- # 為什麼 JavaScript 會有那麼多內建語法可以使用? - 原型鏈 當我們撰寫 JavaScript 的程式碼時,為什麼會有那麼多的內建方法可以用? 例如一組陣列可以使用 `.length` 來得知陣列有多少位址資料,陣列內建的方式又是從何而來? 這裡紀錄一些關於原型的筆記 : - 原型物件 - 原型屬性 - 原型繼承 - 原型鏈 - new Object( ) 自訂原型 - Object.create( ) 將其他物件作為原型 - 多層繼承實作 - 物件屬性特徵 ## 原型物件 JavaScript 是一個物件導向的語言,許多內建的方法都是透過繼承原型屬性來的,當我們打開以 JS 撰寫的瀏覽器時,就建好基礎的原型方法。像是以下例子,原型的頂層為一個物件,其名稱為 **Object**。物件 `{}`、函式 `function`、陣列 `[]` ,都是繼承了這個原型物件內的屬性,再根據型別有自己的原生方法,例如陣列與函式的原生原型就不一樣。 ```javascript console.log(Object) // 此為物件頂層,會發現一個名為 Object的函式 // 函式也是物件的一種,可以發現它擁有自己的物件屬性 console.log(Object()) // 執行 Object() 這個函式會 return 一個名為 Object的物件 console.log(Array.__proto__ === Function.__proto__) // true,__proto__ 都是繼承了物件 console.log(Array.prototype === Function.prototype) // false,原生原型不同 ``` ### 原型屬性 原型物件內建了很多原生屬性,我們可以自行增加共用的屬性,方式有兩個 : 1. `__proto__` ,繼承原型,建立在物件上 - 每個物件都有此屬性 - 屬性特徵為可被列舉 - **原型鏈的關鍵** 2. `prototype` ,原生原型,建立在函式上 - 函式才有此原生屬性 - 屬性特徵為不可被列舉 為什麼以下比對是 true? ```javascript console.log( Object.__proto__.__proto__ === Object.prototype ) // true ``` 因為物件原型 `Object` 本身是一個函式,而 - `Object` 這個函式繼承了 `function` 的原生原型 - `function` 又繼承了原型物件 `Object` 的原生原型 所以它們指得是同樣的東西,可以使用 `console.dir(Object)` 驗證。(可以先往下看原型繼承,再回來想一下。) --- ### 原型繼承 以下比對可以了解繼承原型與原生原型。 首先,物件、函式與陣列本身為 ``"function"`` 。 ```javascript console.log(typeof Object) // "function" console.log(typeof Array) // "function" console.log(typeof Function) // "function" ``` #### Object 原生原型與繼承原型 ```javascript /* Object */ console.log( Object.prototype === Object.__proto__ ) // false,前者是物件原生原型之物件,後者是繼承函式原型之函式 // Object,本身是為名為 Object的函式 console.log( Object.__proto__.__proto__ === Object.prototype ) // true,前者是繼承函式原型又繼承物件原型之物件,後者是原生原型之物件 ``` #### Function 原生原型與繼承原型 ```javascript /* Function */ console.log( Function.prototype === Object.__proto__ ) // true,前者是函式原生原型之函式,後者是繼承函式原型之函式 console.log( Function.prototype === Function.__proto__ ) // true,前者為函式原生原型之函式,後者為繼承函式原型之函式 console.log( Function.__proto__ === Object.__proto__ ) // true console.log( Function.__proto__.__proto__ === Object.__proto__.__proto__ ) // true ``` #### Array 原生原型與繼承原型 ```javascript /* Array */ console.log( Array.prototype === Object.__proto__ ) // false,前者為陣列原生原型之物件,後者為繼承函式原型之函式 // typeof Array.prototype 為 "object" console.log( Array.prototype === Array.__proto__) // false,前者為陣列原生原型之物件,後者為繼承函式原型之函式 ``` #### Function and Array 原生原型與繼承原型 ```javascript /* Function and Array*/ console.log( Function.prototype === Array.prototype ) // false,原生原型不同 console.log( Function.__proto__ === Array.__proto__ ) // true,均為繼承函式原型之函式 console.log( Function.__proto__.__proto__ === Array.__proto__.__proto__ ) // true ``` 在往後撰寫 JS程式碼時,就可以知道正在使用的陣列、函式以及物件是繼承誰的屬性。 --- ## 原型鏈 - 擁有物件的特性 - 會往上層搜尋 - 原型屬性可共用,不須額外撰寫 - 可以新增屬性在原型中並賦予值,可以是陣列、函式、物件、純值等等... - 其他物件可以利用點記號取用 例如下面的範例 : ```javascript var arrayA = [1, 2, 3] var arrayB = [4, 5, 6] arrayA.name = 'X陣列' arrayB.name = 'Y陣列' arrayA.getLast = function(){ return this.name } console.log(typeof arrayA) console.log(arrayA.getLast()) console.log(arrayB.getLast()) // "object" // "X陣列" // arrayB.getLast is not a function ``` 上面的範例中 : - `arrayA` ,可以使用 getLast這個方法 - `arrayB` ,卻無法使用 getLast這個方法,因為只建立在 `arrayA` 這個物件下 - 若要讓 `arrayB` ,可以使用 getLast這個方法,就得再建立一次 那如何讓方法共用呢? 我們可以利用**原型鏈**,新增一個共用方法在陣列這個實體物件上,像是這樣 : ```javascript var arrayA = [1, 2, 3] var arrayB = [4, 5, 6] arrayA.name = 'X陣列' arrayB.name = 'Y陣列' // 使用 __proto__ ,其繼承了陣列的原生原型 // 在這個原型屬性上建立一個新方法 arrayA.__proto__.getLast = function(){ return this.name } console.log(arrayA.getLast()) console.log(arrayB.getLast()) // "X陣列" // "Y陣列" ``` 不過上述的例子無法使用 `prototype` ,因為這個實體陣列型別為 "object",它沒有這個原型屬性,只有函式才有,而 `__proto__` 直接在繼承來的物件上新增共用方法。除非新增在原生原型 `Array.prototype.getLast()`,這樣所有陣列都能共用。 --- ## new Object( ) 自訂原型 只需要在原型自訂方法,就不用浪費一堆記憶體空間(不用一直重開)。 舉個例子來說,打開某個編輯應用程式時,會問你是否套入範本,然後再問你基本資料要填入甚麼。 以下範例是一個構造函式,把它想成是一範本,利用這個範本去建立一個實體檔案,這個範本函式裏頭可以先寫上未來要成為物件時,所擁有的屬性,以建構式運算子建立時,就會成為該物件的屬性。而範例中 : - 每個物件實體繼承了構造函式的屬性名稱,同時也繼承了物件的原生屬性 - 每個物件實體有自己的屬性值 - 不同物件時體會新開記憶體位置 - 每個物件實體之物件屬性為自己的原生原型 ```javascript function motorCycle(name, color, displacement){ this.name = name this.color = color this.displacement = displacement this.congratulate = function(){ console.log('Congratulations on the deal! Have a nice trip!') } } var motoA = new motorCycle('Z900RS', 'red', 948) var motoB = new motorCycle('R1000', 'blue', 1000) console.log(motoA.name) console.log(motoB.color) motoB.congratulate() console.log(motoA.congratulate === motoB.congratulate) // "Z900RS" // "blue" // "Congratulations on the deal! Have a nice trip!" // false ``` 雖然上述範例中,每個物件實體都能使用相同的方法,但它們屬於不同的記憶體位址,直接寫在構造函式中也不易閱讀,接著讓我們來改寫上述的範例 : - 將構造函式的方法移動到原生原型上,也就是物件原生原型 - 結果變成了 true,代表它們參考同樣的位址 ```javascript function motorCycle(name, color, displacement){ this.name = name this.color = color this.displacement = displacement } motorCycle.prototype.congratulate = function(){ console.log('Congratulations on the deal! Have a nice trip!') } var motoA = new motorCycle('Z900RS', 'red', 948) var motoB = new motorCycle('R1000', 'blue', 1000) console.log(motoA.congratulate === motoB.congratulate) console.log(motoA.__proto__ === motorCycle.prototype ) console.log(motorCycle.__proto__ === Function.prototype ) // true // true,motoA繼承了 motorCycle的原生原型,但沒有繼承函式的原生原型 // true,motorCycle繼承了 Function的原生原型 ``` ### 建構式物件繼承了甚麼 不知道看到這裡會不會有個疑問,`motoA` 這個物件實體怎麼沒有繼承函式的原生原型,讓我們用 `console.log(motoA.__proto__)` 檢查它繼承了甚麼? - `congratulate` - 繼承 motorCycle的原生原型之自訂方法 - `constructor` - 繼承 motorCycle這個函式 - 可以利用它來**辨認是從哪一個建構函式繼承的** - `__proto__` - 繼承物件的原生原型 - 建構式物件型別為 "object" ### 如何得知該方法為物件所有還是繼承的 可以利用 `hasOwnProperty` 來檢查。 ```javascript var array = [1, 2] array.forEach((item)=>{ console.log(item) }) console.log( array.hasOwnProperty('forEach')) console.log( array.hasOwnProperty('length')) console.log( array.__proto__.hasOwnProperty('forEach')) // 1 // 2 // false // true // true ``` --- ## Object.create( ) 將其他物件作為原型 除了使用構造函式轉為建構物件來自訂原生原型屬性,也可以將另一個物件直接作為建構物件使用。 1. 自訂構造函式 > 建構物件 > 實體物件 2. 將另一個物件屬性作為建構物件 > 實體物件 - 新的實體物件透過 `Object.create(另一物件)` 將其作為原生原型屬性(包含屬性值) ```javascript= function motorCycle(name, color, displacement){ this.name = name this.color = color this.displacement = displacement } motorCycle.prototype.congratulate = function(){ console.log('Congratulations on the deal! Have a nice trip!') } var motoA = new motorCycle('Z900RS', 'red', 948) var motoB = new motorCycle('R1000', 'blue', 1000) var motoC = Object.create(motoA) motoC.color = 'black' console.log(motoC) console.log(motoC.prototype === motoA.prototype) // true // 原生原型看起來相同,但要注意,建構物件型別為 "object" // 也就是說,它們沒有 prototype這個屬性,是 undefined console.log(motoC.__proto__ === motoA) // true // 繼承另一個物件屬性作為原型 ``` 但是上面的範例這樣做會有點問題,繼承來的原生原型是一個物件,若不變更屬性值,基本上它們的 `console.log()` 內容一樣,但它是利用原型鏈向上層找,本身沒有自己的屬性,像這樣 : ```javascript= /* 這是 motoC */ console.log(motoC) // 在開發者模式下的結果 // motorCycle { // __proto__ : motorCycle // } /* 這是 motoA */ console.log(motoA) // motorCycle { // name: 'Z900RS' // color: 'red' // displacement: 948 // __proto__ : Object // } ``` 不過,還是可以利用 `Object.create()` 來快速複製屬性,例如繼承多個物件屬性時會用到它。 ## 多層繼承實作 JS無法同時繼承多個物件,但是可以使用多層繼承的方式,實作中我們會用到 : - `Object.creat()` - `new Object()` - `prototype` ```javascript // Object > Transportation > 汽車、摩托車、飛機 > 保時捷911、Z900RS、A320 /* 交通工具 */ function Transportation(tool){ this.toolName = tool || '交通工具' } // 在頂層交通工具函式原型上新增共用方法 Transportation.prototype.move = function(){ console.log(this.toolName + '向前移動') } Transportation.prototype.congratulate = function(){ console.log('Congratulations on the deal! Have a nice trip!') } /* 交通工具類別*/ function motorCycle(name, color, displacement){ // 執行 Transportation.call() 並填入工具名稱,完成該工具類別屬性 // 未加入任何原型 Transportation.call(this, '摩托車') this.name = name this.color = color this.displacement = displacement } // 將交通工具類別的原型更改為頂層交通工具 motorCycle.prototype = Object.create(Transportation.prototype) // motorCycle.prototype = Transportation.prototype // 這樣也是可以運作,但是這樣變成傳參考,修改 motorCycle.prototype時, // 也會一併修改 Transportation.prototype // 將交通工具的類別的建構函式改回自己,否則為頂層交通工具的建構函式 // 有助於辨別從哪個函式轉來 motorCycle.prototype.constructor = motorCycle // 將交通工具的類別,新增一個只有該類別才能用的函式 motorCycle.prototype.wheelie = function (){ console.log('翹孤輪~') } // 雖然它的繼承原型還是 Transportation, // 但是同一層的建構函式已修改回原本交通工具類別,還是可以辨認 var motoA = new motorCycle('Z900RS', 'red', 948) motoA.move() // "摩托車向前移動" motoA.wheelie() // "翹孤輪~" ``` ## 物件屬性特徵 當我們使用原型來新增方法時,有沒有發現到有個東西會用到,但實際上看不到它? ### prototype 當我們新增一個方法在函式原型上,為什麼使用 console.log()時,卻找不到 `prototype` 呢? 讓我們使用 `Object.getOwnPropertyDescriptor` 這個方式來檢查該 prototype 這個物件屬性, ```javascript // 創造一個構造函式 function proto(name){ this.name = name } // 接著,在原型上新增一個方法 proto.prototype.fnA = function (){ console.log('函式') } console.log(Object.getOwnPropertyDescriptor(proto, 'prototype')) // Object { // value: { // fnA: funciton(){ // console.log('函式') // }, // constructor: ƒ proto(name), // __proto__: Object{...} // } // writable: true, // 可寫入 // configurable: false, // 不可被刪除 // enumerable: false, // 不可被列舉 // __proto__: Object{...} // } ``` 我們可以發現,`prototype` 這個屬性設定的其中兩個為 false - configurable 可否被刪除 - enumerable 可否被列舉 很多教學建議新增原型方法時,盡量設定在這個屬性上,但實際上又看不到它,而上述就是為什麼看不到 `prototype` 這個屬性的原因。 <!-- ## new Object() 和 Object.create() - 建立屬性的位置不同 - 屬性的設定不同 ### 透過 new Object() - 建立在自身屬性上 - 屬性設定均為 true ### 透過 Object.create() - 建立在 `__proto__` 這個原型屬性上 - 屬性設定均為 false --> ## 參考來源 >1. 六角學院 - JS核心篇 >2. [Huli- 該來理解 JavaScript 的原型鍊了](https://blog.techbridge.cc/2017/04/22/javascript-prototype/) >3. [MDN - 繼承與原型鏈](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) >4. [深入剖析Object.create(),為與之相關的理解形成閉環](https://www.mdeditor.tw/pl/p77l/zh-tw)