[JS] 原型
TL;DR
參考資料
- 物件導向基本觀念 | ALPHA CAMP
相關連結
- Object.prototype.proto | MDN
- instanceof | MDN
- Object.create() | MDN
建構函式
Javascript原本就提供一系列建構函式,例如Object
、String
、Boolean
、Date
,Promise
...etc。
一般來說我們習慣用大寫來表示建構函式, 例如Date
。
所以如果我們自己建立建構函示時,雖然使用小寫並不會產生任何錯誤,也會建議使用大寫。
建立自己的建構函示
建構函示其實與一般函示沒有差別,都是使用function
來宣告,我們來簡單創建一個建構函式:
function Dog(name,size,color){
this.name=name
this.size=size
this.color=color
}
接著我們使用new
運算子來透過這個Dog
建構函示建立實體(instance)
const white=new Dog('小白','小型犬','白色')
console.log(white) // Dog {name: '小白', size: '小型犬', color: '白色'}
使用new
運算子時,會進行以下動作:
- 創建一個空物件實體
{}
- 接著將該函式的
this
指向步驟一創建的空物件 - 依序執行(e.g:this.name=name會在物件內創建一個name屬性,並且將參數name的值賦予到該屬性上)
經過以上動作,我們建立了一個建構函示Dog
的原型,並且利用new
運算子生成一個名叫小白的小型犬實體。
[[prototype]]
延續上一段的程式碼,我們先來觀察剛剛建立好的實例,因為是一個物件所以可以展開如下:
> Dog
color: "白色"
name: "小白"
size: "小型犬"
Dog原型
Safari的Dog實例
Chrome的Dog實例
[[prototype]]
指向的是目前這個實例的原型,以我們上方範例中,我們的white
實例的原型即為Dog
建構函示。
在自己創建的建構函示實例這邊,實例的[[prototype]]
只有Safari會明確的寫出是 Dog原型
,其他如Chrome,Firefox,Arc等等瀏覽器都會顯示為 Object
。
那麼[[prototype]]
能做到什麼事情呢? 我們知道所有的實例都會透過原型鏈鏈聯結起來。我們在[[prototype]]內希望新增一個汪汪叫的方法。
在撰寫方法之前,要先知道[[prototype]]
在一些瀏覽器內可能會用__proto__
代替,這兩個指的是同一個東西。
需要注意的是,__proto__
的用法已經從Web標準中移除,所以不建議再繼續使用,應該改用Object.getPropertyOf
(或是Reflect.getPrototypeOf()
)來取代
- Object.prototype.proto | MDN
__proto__
新增方法
因為dog.__proto__
本身是一個物件
console.log(typeof white.__proto__); // 'object'
const ary=[1,2,3];
console.log(typeof ary.__proto__); // 'object'
既然是物件,我們就可以在裡面新增一個方法:
white.__proto__.bark=function(){
console.log(`${this.name} 汪汪叫!!!`);
}
新增這個汪汪叫的方法之後,我們就可以讓剛剛建立好的實例(小白)汪汪叫:
white.bark(); // 小白 汪汪叫!!!
而且我們再次透過new
運算子建立一個新的實例小黑,小黑也可以透過原型鏈的方式,找到bark這個方法,所以小黑也可以汪汪叫了:
const black=new Dog('小黑','大型犬','黑色');
console.log(black); // Dog {name: '小黑', size: '大型犬', color: '黑色'}
black.bark(); // 小黑 汪汪叫!!!
最後我們可以來觀察一下小黑跟小白的[[prototype]]
:
console.log(white.__proto__); // {bark: ƒ}
console.log(black.__proto__); // {bark: ƒ}
console.log(white.__proto__===black.__proto__); // true
Object.getPrototypeOf()
新增方法
因為不建議繼續使用__proto__
,所以我們要來介紹Object.getPrototypeOf()
:
console.log(typeof Object.getPrototypeOf(white)); // 'object'
console.log(white.__proto__===Object.getPrototypeOf(black)); // true
如果希望透過Object.getPrototypeOf
來新增的話寫法如下
Object.getPrototypeOf(white).eatbone=function(){
console.log(`${this.name} 啃骨頭`);
}
black.eatbone(); //小黑 啃骨頭
- Object.getPrototypeOf() | MDN
- Reflect.getPrototypeOf() | MDN
靜態方法 Reflect.getPrototypeOf()
與 Object.getPrototypeOf()
方法幾乎是一樣的,都是傳回指定物件的原型(即內部的 [[Prototype]] 屬性的值)。
通常我們只要記得Object.getPrototypeOf()
方法即可。
這邊有提到靜態方法,後續會介紹。
由建構函式新增方法(推薦)
其實除了上述兩種新增方法以外,還有另一種更推薦的方式 - 直接透過建構函示來新增。
前面我們不管是透過Object.getPrototypeOf()
、__proto__
,都是取得一個物件,並且在該物件下新增方法。其實我們也可以直接透過建構函示取得:
console.log(typeof Dog.prototype); // 'object'
console.log(Dog.prototype===white.__proto__); // true
console.log(Dog.prototype===Object.getPrototypeOf(black)); // true
也就是說,我們在建構函式的prototype
屬性內所新增的方法,會對應到該建構函示生成實例的[[prototype]]
內!
Dog.prototype
內的prototype,與[[prototype]]
是完全不一樣的東西!!
我們可以透過建構函式的prototype來新增方法如下:
Dog.prototype.getInfo=function(){
console.log(`我叫${this.name},我是${this.size},我的毛色是${this.color}`);
}
調用方法:
console.log(white.getInfo()); // 我叫小白,我是小型犬,我的毛色是白色
console.log(black.getInfo()); // 我叫小黑,我是大型犬,我的毛色是黑色
注意事項
需要注意,在定義方法的時候不可以使用箭頭函式(arrow function),而 是要使用函式表達式(匿名函式)。
這是因為我們如果使用箭頭函式定義方法的話,可能this的指向會有誤,在simple call的情況下(不考慮'use strict'嚴格模式),this
的指向會是window
。
// 正確範例
const ary1=[1,2,3,4,5]
Array.prototype.getLastElement=function(){
return this[this.length-1]
}
console.log(ary1.getLastElement()) //5
//錯誤範例
const ary2=[6,7,8,9]
Array.prototype.getFirstElement=()=>{
return this[0];
}
console.log(ary2.getFirstElemnt()) //error!!! expect 6, but undefined
console.log(ary2.getLastElement()) //9
我們首先先直接宣告一個陣列,並且利用Array建構函示來新增一個取得最後一個元素的方法。
因為在定義getFirstElement
時使用箭頭函式,this在這個狀況下指向window,所以this[0]會是undefined。並且我們也同樣可以在ary2上使用先前所定義的getLastElement
方法
constructor
觀察white
的內容時,可以發現[[prototype]]
內現在除了我們剛剛透過三種方式新增的三個方法以外,還有另一個屬性 - constructor
。
在white
的[[prototype]]
內,另外也有另一個[[prototype]]
,指向原始物件原型的prototype。
Javascript就是透過原型鏈的方式來實現的,所以我們透過Dog所產生的實例,也可以使用到Object.prototype
內所定義的方法!
white.__proto__.__proto__===Object.prototype // true
我們在建立建構函式的時候,在建構函式的prototype內就會存放一個屬性constructor
,內容指向建構函示本身。
Dog.prototype.constructor===Dog // ture
後續我們會介紹到使用Object.create
的方式來創建兩層以上的自訂原型鏈,到時候我們會需要重新手動調整constructor
的指向。
instanceof
- 🔗instanceof | MDN
instanceof
運算子用於檢測建構函式的prototype
屬性是否出現在某個實例物件的原型鏈上。
用法如下:
function Dog(name,size,color){
this.name=name
this.size=size
this.color=color
}
const white=new Dog('小白','小型犬','白色');
const ary=[1,2,3,4];
console.log(white instanceof Dog) // true
console.log(white instanceof Object) //true
console.log(ary instanceof Dog) // false
console.log(ary instanceof Object) // true
簡單來說,因為white.__proto__.constructor
是Dog
,所以white instanceof Dog
會是true
。
同理,white.__proto__.__proto__.constructor
是Object
,所以white instanceof Object
也會是true
。
使用instanceof
來檢查是不是該建構函式的實例,其實就是在檢查他的原型鏈上的constructor有沒有指向該建構函式
當然,因為ary
透過原型鏈查找(即ary.__proto__
)會先找到Array.prototype
,再往上一層去尋找(即ary.__proto__.__proto__
)會查找到Object.prototype
。中間並不會有Dog.protoype
!
所以ary instanceof Dog
就會是false
!
在原型鏈向上尋找的過程中,使用Object.getPrototypeOf()
的寫法會不易閱讀:
Object.getPrototypeOf(Object.getPrototypeOf(ary))===Object.prototype // true
ary.__proto__.__proto__===Object.prototype //ture
兩者的寫法是相同的,但是為了閱讀方便,仍然採用__proto__
來撰寫筆記。
原型鏈
在前面我們已經學會如何自己撰寫一個建構函式,並且透過該建構函式生成實例。目前我們的Dog
建構函式,上一層是連結到Object
。
Object.getPrototypeOf(Dog.prototype)===Object.prototype // true
//or
Dog.prototype.__proto__===Object.prototype // true
如果我們希望在Dog.prototype
和Object.prototpe
中間額外再插入一層Animal.prototype
,並且在Animal.prototype
新增一個方法該怎麼做?
首先先來了解Object.create
可以做到什麼事
Object.create
- 🔗Object.create() | MDN
Object.create
可以直接指 定一個物件作為原型:
const Foo={
myName:'Foo的myName',
fn(){
console.log(this.myName)
}
}
const bar = Object.create(Foo);
console.log(bar) //Object { }
bar.fn(); // Foo的myName
bar.myName='bar的myName';
console.log(bar) // { myName: "bar的myName" }
bar.fn(); // bar的myName
因為我們透過Object.create
創建出bar
,所以bar
雖然是空物件,但是bar
的原型會指向Foo
物件。也因為如此,所以我們調用bar.fn()
時,執行的console.log(this.myName)
當中的myName
其實是bar.__proto__.myName
。
當我們手動在bar
直接新增myName
屬性後,在調用bar.fn()
時,因為bar
本身就有myName
屬性,所以不需向上往原型鏈查找,最後會印出'bar的myName'
。
需要注意的是,這邊的bar原型內 不會存在constructor
,因為是直接指定物件當作原型。
實作原型鏈
我們知道Object.create
的用法之後,接著我們要來建立自己的原型鍊了。做法是透過Object.create
,來將建構函式的prototype給串聯起來。我們先建立上層原型:
function Animal(family='人科'){
this.kindom='動物界';
this.family=family
}
Animal.prototype.move=function(){
console.log(`${this.name} 移動`)
}
先建立好希望當作上層原型的動物界原型。,並且在該原型新增一個走動(move)的方法。
接著透過Object.create
串連:
function Dog(name,size,color){
this.name=name
this.size=size
this.color=color
}
Dog.prototype=Object.create(Animal.prototype);
最後,我們要把Dog
原型可以使用的方法給加回來:
Dog.prototype.bark=function(){
console.log(`${this.name} 汪汪叫`);
}
現在我們如果使用new
運算子新增一隻小狗:
const bibi=new Dog('bibi','小型犬','灰色');
最後,我們來測試bibi這個由狗建構函式建立的實例,分別能不能正確使用到屬於Dog
方法的bark
跟屬於Animal
方法的move
:
bibi.bark(); // bibi 汪汪叫
bibi.move(); // bibi 移動
雖然目前可以讓實例bibi正確使用Dog
跟Animal
的方法,但是我們有在Animal
建構函式內定義的kindom
屬性並沒有繼承到bibi身上:
console.log(bibi.kindom); // undefined
問題出在於,我們有讓Dog
的原型繼承在Animal
的原型下,但是並沒有讓Dog
的建構函式與Animal
的建構函式產生關聯,要修正這個問題我們需要調整Dog
的建構函式如下:
function Dog(name,size,color){
Animal.call(this,'犬科');
this.name=name
this.size=size
this.color=color
}
調整完Dog
建構函式之後,重新建立一個新的實例:
const yellow=new Dog('小黃','中型犬','黃色');
console.log(yellow) // Dog {kindom: "動物界", family: "犬科", name: "小黃", size: "中型犬", color: "黃色"}
因為我們修正了Dog
建構函示,在使用new
運算子創建一個新的空物件並動態將this
指向這個空物件,我們接著使用Animal.call
並且將指向剛剛創建的空物件的this
當成thisArg
傳入,接著將'犬科'當作參數也傳入並執行Animal建構函式。
這邊需要先了解Function.prototype.call(MDN)。
目前程式碼都可以正確運行了,但是因為我們在建立原型鏈的時候,手動賦予Dog.prototype
。因為是手動賦予的關係,目前Dog.prototype
並不具有constructor
。
所以我們要手動補上這個constructor
:
Dog.prototype.constructor=Dog;
實作完整程式碼
上方為了示範過程,中間有許多回頭修正的地方,所以以下附上一段完整的程式碼,實作出white(instance)->Dog->Animal
的結構:
// 定義Animal建構函式
function Animal(family='人科'){
this.kindom='動物界';
this.family=family
}
// 新增Animal原型方法
Animal.prototype.move=function(){
console.log(`${this.name} 移動`)
}
// 定義Dog建構函式(需callAnimal建構函式)
function Dog(name,size,color){
Animal.call(this,'犬科');
this.name=name
this.size=size
this.color=color
}
// 建立Dog.prototype->Animal.prototype的原型鏈結構
Dog.prototype=Object.create(Animal.prototype);
// 將constructor綁回Dog建構函式
Dog.prototype.constructor=Dog;
// 新增Dog原型方法
Dog.prototype.bark=function(){
console.log(`${this.name} 汪汪叫`);
}
// 需注意Dog原型的方法一定要在透過Object.create綁定過後才可以新增,否則會被覆蓋掉
// 建立實例(instance)
const white=new Dog('小白','小型犬','白色');
white.bark(); // 小白 汪汪叫
white.move(); // 小白 移動
// 如果後方想繼續新增原型方法,不管是Animal的原型方法或Dog的原型方法皆可,因為原型鏈已經建立好了。
// 提供程式碼測試在原型建立過後,新增其他方法的測試
// Animal.prototype.foo=function(){
// console.log(`${this.name} ~ foo`)
// }
// Dog.prototype.bar=function(){
// console.log(`${this.kindom}~ bar`)
// }
// white.foo();
// white.bar();
必須要先透過Object.create
建立原型鏈後,才可以調整下層的原型方法(在範例中即為Dog.prototype
)。
測試:實例(white)產生後才調整Animal原型方法,已產生實 例是否可以使用
結論:可以使用。
function Animal(family='人科'){
this.kindom='動物界';
this.family=family
}
function Dog(name,size,color){
Animal.call(this,'犬科');
this.name=name
this.size=size
this.color=color
}
Dog.prototype=Object.create(Animal.prototype);
Dog.prototype.constructor=Dog;
const white=new Dog('小白','小型犬','白色');
Animal.prototype.move=function(){
console.log(`${this.name} 移動`)
}
white.move() // 小白 移動
測試:不修正constructor
是否會影響instanceof
的運作?
constructor
是否會影響instanceof
的運作?結論:不影響。
function Animal(family='人科'){
this.kindom='動物界';
this.family=family
}
function Dog(name,size,color){
Animal.call(this,'犬科');
this.name=name
this.size=size
this.color=color
}
Dog.prototype=Object.create(Animal.prototype);
const white=new Dog('小白','小型犬','白色');
white instanceof Dog // true
white instanceof Animal // true
實作一個動物界原型
目前已經學習到如何透過Object.create
製作出一個原型鏈。接著我們希望可以新增一個貓科,同樣隸屬於動物界之下。
關係圖如下:
// 定義Animal建構函式
function Animal(family='人科'){
this.kindom='動物界';
this.family=family
}
// 新增Animal原型方法
Animal.prototype.move=function(){
console.log(`${this.name} 移動`)
}
// 定義Dog建構函式
function Dog(name,size,color){
Animal.call(this,'犬科');
this.name=name
this.size=size
this.color=color
}
// 建立Dog.prototype->Animal.prototype的原型鏈結構
Dog.prototype=Object.create(Animal.prototype);
// 將constructor綁回Dog建構函式
Dog.prototype.constructor=Dog;
// 新增Dog原型方法
Dog.prototype.bark=function(){
console.log(`${this.name} 汪汪叫`);
}
// 需注意Dog原型的方法一定要在透過Object.create綁定過後才可以新增,否則會被覆蓋掉
// 定義Cat建構函式
function Cat(name,size,color){
Animal.call(this,'貓科');
this.name=name
this.size=size
this.color=color
}
// 建立Cat.prototype->Animal.prototype的原型鏈結構
Cat.prototype=Object.create(Animal.prototype);
// 將constructor綁回Cat建構函式
Cat.prototype.constructor=Cat;
// 新增Cat原型方法
Cat.prototype.meow=function(){
console.log(`${this.name} 喵喵叫`);
}
接著我們創建實例(instance)並且調用方法:
const bibi=new Dog('小白','小型犬','白色');
const mi=new Cat('小咪','小貓','三花');
bibi.bark(); // 小白 汪汪叫
bibi.move(); // 小白 移動
mi.meow(); // 小咪 喵喵叫
mi.move(); // 小咪 移動
// 無法調用不是屬於自己原型的方法
bibi.meow(); // Uncaught TypeError: bibi.meow is not a function
mi.bark(); // Uncaught TypeError: mi.bark is not a function
小結
基本原型鏈觀念驗證
我們透過上方的動物界原型可以得到以下的原型鏈關係圖:
在左上方中,我們建立了一個狗的實例,也就是Bibi。
Bibi本身具有從動物界建構函示繼承的family屬性,以及狗建構函式的顏色等屬性(圖片中因空間關係沒有完整呈現)。從__proto__
中可以查找到狗建構函式原型(i.e:Dog.prototype
)所具有的方法。所以Bibi也可以透過bibi.bark()
來吼叫。最後,Dog.prototype
之下會有一個constructor
屬性,指向該建構函式本身(i.e : function Dog
)。
接著,因為Dog.prototype
是透過Object.create
繼承自Animal.prototype
,所以我們也可以再次透過Dog.prototype.__proto__
找到Animal.prototype
。因此,Bibi也具有移動的能力(可以調用bibi.move()
)。同樣的,Animal.prototype
內也會有一個constructor
屬性,指向建構函式自身(i.e : function Animal
)
如果Animal.prototype
再透過__proto__
向上查找,則會找到物件建構函式的原型Object.prototype
,同樣的,Object.prototype.constructor
也會指向Object
建構函式。
最後,物件原型因為已經是原型鏈的頂層了,所以再向上查找(Object.prototype.__proto__
)只會找到null
。
可以透過以下程式碼驗證:
實作原型鏈:
function Animal(family='人科'){
this.kindom='動物界';
this.family=family
}
Animal.prototype.move=function(){
console.log(`${this.name} 移動`)
}
function Dog(name,size,color){
Animal.call(this,'犬科');
this.name=name
this.size=size
this.color=color
}
Dog.prototype=Object.create(Animal.prototype);
Dog.prototype.constructor=Dog;
Dog.prototype.bark=function(){
console.log(`${this.name} 汪汪叫`);
}
const bibi=new Dog('小白','小型犬','白色');
驗證:
console.log(bibi.__proto__===Dog.prototype) // ture
console.log(bibi.__proto__.constructor===Dog) // true
console.log(bibi.__proto__.__proto__===Animal.prototype) //ture
console.log(Dog.prototype.__proto__===Animal.prototype) // true
console.log(Animal.prototype.constructor===Animal) //true
console.log(bibi.__proto__.__proto__.__proto__===Object.prototype) //true
console.log(Animal.prototype.__proto__===Object.prototype) // true
console.log(Object.prototype.constructor===Object) // true
console.log(bibi.__proto__.__proto__.__proto__.__proto__===null) // true
建構函式的原型練
建構函式本身也有屬於他的原型鏈
Dog
,Animal
,Object
等等的建構函式,也可以透過__proto__
去向上查找到 Function.prototype
。
同樣的,我們可以在Function.prototype
內透過constructor
找到Function
建構函式。
最後,Function.prototype
向上查找會找到物件的原型(Object.protoype
)。
Function
很特殊,他自身也是屬於建構函式,所以也可以透過Function.__proto__
查找到Function.prototype`。形成一個circle。
可以透過以下方式驗證:
console.log(Function.__proto__===Function.prototype); // true
可以透過以下程式碼驗證:
實作原型鏈:
// 這邊只是要驗證建構函式自身原型鏈,所以不需要方法及屬性,也不需要讓Dog繼承Animal建構函式所有屬性
function Animal(){}
function Dog(){}
Dog.prototype=Object.create(Animal.prototype);
Dog.prototype.constructor=Dog;
const bibi=new Dog();
驗證:
console.log(Dog.__proto__===Function.prototype); // true
console.log(Animal.__proto__===Function.prototype); // true
console.log(Object.__proto__===Function.prototype); // true
console.log(Function.__proto__===Function.prototype); // true
console.log(Function.__proto__.__proto__===Object.prototype); // true
console.log(Function.prototype.__proto__===Object.prototype); // true