當我們撰寫 JavaScript的程式碼時,為什麼會有那麼多的內建方法可以用? 例如一組陣列可以使用 .length
來得知陣列有多少位址資料,陣列內建的方式又是從何而來?
這裡紀錄一些關於原型的筆記 :
原型物件
原型鏈
new Object( ) 自訂原型
Object.create( ) 將其他物件作為原型
多層繼承實作
物件屬性特徵
原型物件 JavaScript是一個物件導向的語言,許多內建的方法都是透過繼承原型屬性來的,當我們打開以 JS撰寫的瀏覽器時,就建好基礎的原型方法。像是以下例子,原型的頂層為一個物件,其名稱為 Object 。物件 {}
、函式 function
、陣列 []
,都是繼承了這個原型物件內的屬性,再根據型別有自己的原生方法,例如陣列與函式的原生原型就不一樣。
1 2 3 4 5 6 7 8 9 10 11 12 console .log(Object ) console .log(Object ())console .log(Array .__proto__ === Function .__proto__) console .log(Array .prototype === Function .prototype)
原型屬性 原型物件內建了很多原生屬性,我們可以自行增加共用的屬性,方式有兩個 :
__proto__
,繼承原型,建立在物件上
每個物件都有此屬性
屬性特徵為可被列舉
原型鏈的關鍵
prototype
,原生原型,建立在函式上
為什麼以下比對是 true?
1 2 console .log( Object .__proto__.__proto__ === Object .prototype )
因為物件原型 Object
本身是一個函式,而
Object
這個函式繼承了 function
的原生原型
function
又繼承了原型物件 Object
的原生原型
所以它們指得是同樣的東西,可以使用 console.dir(Object)
驗證。(可以先往下看原型繼承,再回來想一下。)
原型繼承 以下比對可以了解繼承原型與原生原型。
首先,物件、函式與陣列本身為 "function"
。
1 2 3 console .log(typeof Object ) console .log(typeof Array ) console .log(typeof Function )
Object 原生原型與繼承原型 1 2 3 4 5 6 7 console .log( Object .prototype === Object .__proto__ ) console .log( Object .__proto__.__proto__ === Object .prototype )
Function 原生原型與繼承原型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 console .log( Function .prototype === Object .__proto__ ) console .log( Function .prototype === Function .__proto__ ) console .log( Function .__proto__ === Object .__proto__ )console .log( Function .__proto__.__proto__ === Object .__proto__.__proto__ )
Array 原生原型與繼承原型 1 2 3 4 5 6 7 console .log( Array .prototype === Object .__proto__ ) console .log( Array .prototype === Array .__proto__)
Function and Array 原生原型與繼承原型 1 2 3 4 5 6 7 8 9 10 11 12 console .log( Function .prototype === Array .prototype )console .log( Function .__proto__ === Array .__proto__ )console .log( Function .__proto__.__proto__ === Array .__proto__.__proto__ )
在往後撰寫 JS程式碼時,就可以知道正在使用的陣列、函式以及物件是繼承誰的屬性。
原型鏈
擁有物件的特性
會往上層搜尋
原型屬性可共用,不須額外撰寫
可以新增屬性在原型中並賦予值,可以是陣列、函式、物件、純值等等…
其他物件可以利用點記號取用
例如下面的範例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 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())
上面的範例中 :
arrayA
,可以使用 getLast這個方法
arrayB
,卻無法使用 getLast這個方法,因為只建立在 arrayA
這個物件下
若要讓 arrayB
,可以使用 getLast這個方法,就得再建立一次
那如何讓方法共用呢? 我們可以利用原型鏈 ,新增一個共用方法在陣列這個實體物件上,像是這樣 :
1 2 3 4 5 6 7 8 9 10 11 12 13 var arrayA = [1 , 2 , 3 ]var arrayB = [4 , 5 , 6 ]arrayA.name = 'X陣列' arrayB.name = 'Y陣列' arrayA.__proto__.getLast = function ( ) { return this .name } console .log(arrayA.getLast())console .log(arrayB.getLast())
不過上述的例子無法使用 prototype
,因為這個實體陣列型別為 “object”,它沒有這個原型屬性,只有函式才有,而 __proto__
直接在繼承來的物件上新增共用方法。除非新增在原生原型 Array.prototype.getLast()
,這樣所有陣列都能共用。
new Object( ) 自訂原型 只需要在原型自訂方法,就不用浪費一堆記憶體空間(不用一直重開)。
舉個例子來說,打開某個編輯應用程式時,會問你是否套入範本,然後再問你基本資料要填入甚麼。 以下範例是一個構造函式,把它想成是一範本,利用這個範本去建立一個實體檔案,這個範本函式裏頭可以先寫上未來要成為物件時,所擁有的屬性,以建構式運算子建立時,就會成為該物件的屬性。 而範例中 :
每個物件實體繼承了構造函式的屬性名稱,同時也繼承了物件的原生屬性
每個物件實體有自己的屬性值
不同物件時體會新開記憶體位置
每個物件實體之物件屬性為自己的原生原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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)
雖然上述範例中,每個物件實體都能使用相同的方法,但它們屬於不同的記憶體位址,直接寫在構造函式中也不易閱讀,接著讓我們來改寫上述的範例 :
將構造函式的方法移動到原生原型上,也就是物件原生原型
結果變成了 true,代表它們參考同樣的位址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 )
建構式物件繼承了甚麼 不知道看到這裡會不會有個疑問,motoA
這個物件實體怎麼沒有繼承函式的原生原型,讓我們用 console.log(motoA.__proto__)
檢查它繼承了甚麼?
congratulate
constructor
繼承 motorCycle這個函式
可以利用它來辨認是從哪一個建構函式繼承的
__proto__
繼承物件的原生原型
建構式物件型別為 “object”
如何得知該方法為物件所有還是繼承的 可以利用 hasOwnProperty
來檢查。
1 2 3 4 5 6 7 8 9 10 11 12 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' ))
Object.create( ) 將其他物件作為原型 除了使用構造函式轉為建構物件來自訂原生原型屬性,也可以將另一個物件直接作為建構物件使用。
自訂構造函式 > 建構物件 > 實體物件
將另一個物件屬性作為建構物件 > 實體物件
新的實體物件透過 Object.create(另一物件)
將其作為原生原型屬性(包含屬性值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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) console .log(motoC.__proto__ === motoA)
但是上面的範例這樣做會有點問題,繼承來的原生原型是一個物件,若不變更屬性值,基本上它們的 console.log() 內容一樣,但它是利用原型鏈向上層找,本身沒有自己的屬性,像這樣 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 console .log(motoC)console .log(motoA)
不過,還是可以利用 Object.create()
來快速複製屬性,例如繼承多個物件屬性時會用到它。
多層繼承實作 JS無法同時繼承多個物件,但是可以使用多層繼承的方式,實作中我們會用到 :
Object.creat()
new Object()
prototype
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 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(this , '摩托車' ) this .name = name this .color = color this .displacement = displacement } motorCycle.prototype = Object .create(Transportation.prototype) motorCycle.prototype.constructor = motorCycle motorCycle.prototype.wheelie = function ( ) { console .log('翹孤輪~' ) } var motoA = new motorCycle('Z900RS' , 'red' , 948 )motoA.move() motoA.wheelie()
物件屬性特徵 當我們使用原型來新增方法時,有沒有發現到有個東西會用到,但實際上看不到它?
prototype 當我們新增一個方法在函式原型上,為什麼使用 console.log()時,卻找不到 prototype
呢? 讓我們使用 Object.getOwnPropertyDescriptor
這個方式來檢查該 prototype 這個物件屬性,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function proto (name ) { this .name = name } proto.prototype.fnA = function ( ) { console .log('函式' ) } console .log(Object .getOwnPropertyDescriptor(proto, 'prototype' ))
我們可以發現,prototype
這個屬性設定的其中兩個為 false
configurable 可否被刪除
enumerable 可否被列舉
很多教學建議新增原型方法時,盡量設定在這個屬性上,但實際上又看不到它,而上述就是為什麼看不到 prototype
這個屬性的原因。
參考來源
六角學院 - JS核心篇
Huli- 該來理解 JavaScript 的原型鍊了
MDN - 繼承與原型鏈