JavaScript 也是一門物件導向的語言,ES6之前並沒有引入類(class)的概念,像c++ 這種典型的面嚮物件語言都是通過類來建立例項物件,而JavaScript是直接通過建構函式來建立例項。
所以理解兩種繼承模式的差異是需要一定時間的,今天我們就來了解一下原型和原型鏈,在介紹原型和原型鏈之前,我們有必要先了解一下建構函式的知識。
建構函式
建構函式模式的目的就是為了建立一個自定義類,並且建立這個類的例項。
建構函式就是一個普通的函式,建立方式和普通函式沒有區別,不同的是建構函式習慣上首字母大寫。另外就是呼叫方式的不同,普通函式是直接呼叫,而建構函式需要使用new關鍵字來呼叫。我們先使用建構函式建立一個物件:
function Dog() {
this.name = '阿黃'
}
var dog = new Dog()
console.log(dog.name) // 阿黃
複製程式碼
上面例子中,Dog 就是一個建構函式,我們使用 new 建立了一個例項物件 dog。
原型
prototype
JavaScript是一種基於原型的語言(prototype-based language),每個物件擁有一個原型物件,物件以其原型為模板,從原型繼承方法和屬性,這些屬性和方法定義在物件的構造器函式的prototype
屬性上,而非物件例項本身。看以下程式碼:
function Dog() {
this.name = '阿黃'
}
console.log(Dog.prototype)
複製程式碼
那這個建構函式的 prototype
屬性指向的是什麼呢?是這個函式的原型嗎?
開啟 chrome 瀏覽器的開發者工具,在 console 欄輸入上面的程式碼,你可以看到 Dog.prototype
的值:
其實,函式的 prototype
屬性指向了一個物件,這個物件正是呼叫該建構函式而建立的例項的原型。
那什麼是原型呢?你可以這樣理解:每一個JavaScript物件(null
除外)在建立的時候就會與之關聯另一個物件,這個物件就是我們所說的原型,每一個物件都會從原型"繼承"屬性。
讓我們用一張圖來表示建構函式和例項原型之間的關係:
那麼我們該怎麼表示例項與例項原型,也就是 dog
和 Dog.prototype
之間的關係呢,接下來就應該講到第二個屬性:
proto
上面可以看到 Dog 原型(Dog.prototype
)上有__proto__
屬性,這是一個訪問器屬性(即 getter 函式和 setter 函式),通過它可以訪問到物件的內部[[Prototype]]
(一個物件或null
)。
為了證明這一點,我們可以在chrome中輸入:
function Dog() {
this.name = '阿黃'
}
var dog = new Dog()
console.log(Object.getPrototypeOf(dog) === dog.__proto__) // true
console.log(dog.__proto__ === Dog.prototype) // true
複製程式碼
這裡用dog.__proto__
獲取物件的原型,__proto__
是每個例項上都有的屬性,prototype
是建構函式的屬性,這兩個並不一樣,但dog.__proto__
和Dog.prototype
指向同一個物件。於是我們更新下關係圖:
既然例項物件和建構函式都可以指向原型,那麼原型是否有屬性指向建構函式或者例項呢?
constructor
指向例項物件倒是沒有,因為一個建構函式可以生成多個例項,但是原型指向建構函式倒是有的,這就要講到第三個屬性:constructor
,每個原型都有一個 constructor
屬性指向關聯的建構函式。
為了驗證這一點,我們在chrome中輸入:
function Dog() {
this.name = '阿黃'
}
console.log(Dog.prototype.constructor === Dog) // true
複製程式碼
所以再更新下關係圖:
綜上我們已經得出:
function Dog() {
this.name = '阿黃'
}
var dog = new Dog()
console.log(dog.__proto__ == Dog.prototype) // true
console.log(Dog.prototype.constructor == Dog) // true
// 順便學習一個ES5的方法,可以獲得物件的原型
console.log(Object.getPrototypeOf(dog) === Dog.prototype) // true
複製程式碼
原型鏈
在上文我們理解了原型,從字面意思看原型鏈肯定是與原型有關了,是一個個原型連結起來的麼?我們先通過下面的圖來觀察一下。
解析:
obj.prop1:假設我們現在有一個物件,就稱作obj
,而這個物件包含一個屬性(property)
,我們稱作prop1
,現在我們可以使用obj.prop1
來讀取這個屬性的值,就可以直接讀取到prop1
的屬性值了。
obj.prop2:JavaScript中會有一些預設的屬性和方法,所有的物件和函式都包含prototype
這個屬性,假設我們把prototype
叫做proto
,這時候如果我們使用obj.prop2
的時候,JavaScript引擎會先在obj
這個物件的屬性裡去尋找有沒有叫作prop2
的屬性,如果它找不到,這時候它就會再進一步往該物件的proto
裡面去尋找。所以,雖然我們輸入obj.prop2
的時候會得到回傳值,但實際上這不是obj
裡面直接的屬性名稱,而是在obj
的proto
裡面找到的屬性名稱(即,obj.proto.prop2
,但我們不需要這樣打)。
obj.prop3:同樣地,每一個物件裡面都包含一個prototype
,包括物件proto
本身也不例外,所以,如果輸入obj.prop3
時,JavaScript會先在obj
這個物件裡去尋找有沒有prop3
這個屬性名稱,找不到時會再往obj
的proto
去尋找,如果還是找不到時,就再往proto
這個物件裡面的proto
找下去,最後找到後回傳屬性值給我們(obj.proto.proto.prop3
)。
雖然乍看之下,prop3
很像是在物件obj
裡面的屬性,但實際上它是在obj → prop → prop
的物件裡面,而這樣從物件本身往proto
尋找下去的鏈我們就稱作「原型鏈(prototype chain)」。這樣一直往下找會找到什麼時候呢?它會直到某個物件的原型為null
為止(也就是不再有原型指向)。
官方解釋是:每個物件擁有一個原型物件,通過__proto__
指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層,最終指向null
。這種關係被稱為原型鏈 (prototype chain),通過原型鏈一個物件會擁有定義在其他物件中的屬性和方法。
舉個例子來幫助理解原型鏈
讓我們實際來看個例子幫助我們瞭解prototype chain這個概念,這個例子只是單純為了用來說明prototype chain的概念,實際上千萬不要使用這樣的方式程式設計!
首先,我們先建立一個物件person
和一個物件jay
:
var person = {
firstName : 'Default' ,
lastName : 'Default' ,
getFullName : function ( ) {
return this . firstName + ' ' + this . lastName ;
} ,
} ;
var jay = {
firstName : 'Jay' ,
lastName : 'Chou' ,
} ;
複製程式碼
接著,我們知道所有的物件裡面都會包含原型(prototype)這個物件,在JavaScript中這個物件的名稱為__proto__。如同上述原型鏈(prototype chain)的概念,如果在原本的物件中找不到指定的屬性名稱或方法時,就會進一步到__proto__
這裡面來找。
為了示範,我們來對__proto__
做一些事:
//千萬不要照著下面這樣做,這麼做只是為了示範
jay . __proto__ = person ;
複製程式碼
如此,jay
這個物件就繼承了person
物件。在這種情況下,如果我們想要呼叫某個屬性或方法,但在原本jay
這個物件中找不到這個屬性名稱或方法時,JavaScript引擎就會到__proto__
裡面去找,所以當接著執行如下的程式碼時,並不會報錯:
console . log ( jay . getFullName ( ) ) // Jay Chou;
複製程式碼
我們可以得到"Jay Chou"的結果。原本在jay
的這個物件中,是沒有getFullName()
這個方法的,但由於我讓__proto__
裡面繼承了person
這個物件,所以當JavaScript引擎在jay
物件裡面找不到getFullName()
這個方法時,它便會到__proto__
裡面去找,最後它找到了,於是它回傳"Jay Chou"的結果。
如果我是執行:
console . log ( jay . firstName ) ; // Jay
複製程式碼
我們會得到的是John而不是'Default',因為JavaScript引擎在尋找jay.firstName
這個屬性時,在jay
這個物件裡就可以找到了,因此它不會在往__proto__
裡面找。這也就是剛剛在上面所的原型鏈(prototype chain)的概念,一旦它在上層的部分找到該屬性或方法時,就不會在往下層的prototype去尋找。
在瞭解了prototype chain這樣的概念後,讓我們接著看下面這段程式碼:
var jane = {
firstName : 'Jane'
}
jane . __proto__ = person ;
console . log ( jane . getFullName ( ) ) ;
複製程式碼
現在,你可以理解到會輸出什麼結果嗎?
答案是"Jane Default" 。
因為在jane
這個物件裡只有firstName
這個屬性,所以當JavaScript引擎要尋找getFullName()
這個方法和lastName
這個屬性時,它都會去找__proto__
裡面,而這裡面找到的就是一開始建立的person
這個物件的內容。
全程式碼如下:
var person = {
firstName : 'Default' ,
lastName : 'Default' ,
getFullName : function ( ) {
return this . firstName + ' ' + this . lastName ;
}
}
var jay = {
firstName : 'Jay' ,
lastName : 'Chou'
}
//千萬不要照著下面這樣做,這麼做只是為了示範
jay . __proto__ = person ;
console . log ( jay . getFullName ( ) ) ; // Jay Chou
console . log ( jay . firstName ) ; // Jay
var jane = {
firstName : 'Jane'
}
jane . __proto__ = person ;
console . log ( jane . getFullName ( ) ) ;
複製程式碼
以上就是目前能總結的全部了,肯定還是有缺陷的地方,後續還會修改完善的。最後再看底下這張圖,是否有了更深入的理解呢?
如果覺得文章對你有些許幫助,歡迎在我的GitHub部落格點贊和關注,感激不盡!