什麼是原型
原型其實就是一個特殊的物件,在宣告函式的時候自動建立的。
比如,我們現在宣告一個建構函式 A ,除了會申請儲存函式的記憶體空間,還會額外申請一個記憶體空間,用於儲存建構函式 A 的原型物件。所有函式中(Function.prototype.bind 除外)預設都有一個 prototype
的屬性,它儲存了函式的原型物件的地址(引用)(也就是它指向了原型物件)。
而在原型物件中預設有一個 constructor
屬性儲存了建構函式的地址(引用)(也就是 constructor
指向了建構函式)。如果不理解上面所說的,那我們看下面的圖:
瀏覽器控制檯中:
_ _proto_ _
與 prototype
剛開始接觸原型的時候這兩個東西很容易就搞混了。
先記住以下兩點,就很容易就區分了:
prototype
是函式中才有的屬性__proto__
是所有物件都有的屬性
我們已經知道了函式中的 prototype
屬性指向的是它的原型物件,那麼物件中的 __proto__
代表什麼?
一般情況下,物件中的 __proto__
屬性是指向它的建構函式的原型物件的,即和建構函式中的 prototype
屬性所指向的物件是同一個物件。
用一段簡單的程式碼:
function A() {}
var a = new A()
複製程式碼
上圖看著不夠簡便,我們簡化一下:
還有一點,
__proto__
不是一個規範屬性,ie(除了 ie10) 不支援。對應的標準屬性是[[Prototype]]
,但是這個屬性我們沒法直接訪問到。開發者儘量不要用這種方式去訪問,因為操作不慎會改變這個物件的繼承原型鏈。
在使用 Object.create(引數)
方式建立物件時,物件的 __proto__
屬性指向的是傳入的引數。
原型鏈
由於
__proto__
是所有物件都具有的屬性,而__proto__
本身指向的原型(函式.prototype)也是一個物件,它也有__proto__
屬性。所以這樣會形成由__proto__
將物件和原型連起來的鏈條。這就是原型鏈。原型鏈的頂端是Object.prototype
(Object 是所有物件的祖宗) ,Object.prototype.__proto__
的值為null
。
還是看之前的程式碼:
function A() {}
var a = new A()
複製程式碼
它的原型鏈如下:
建構函式 A 其實也是一個物件。所有函式都是由 Function
函式構造的。(宣告函式 function A() {}
等價於 var A = new Function()
) 。所以所有函式的 __proto__
指向的都是 Function.prototype
。更新上圖:
Function
也是一個函式,它的 __proto__
指向的也是 Functon.prototype
即 Funtion.__proto__ === Function.prototype
。繼更新上圖:
Object
同樣是一個函式,所以 Object.__proto__ === Function.prototype
到了這裡,我們應該可以看懂下面這張圖了:
原型的作用
當 JS 引擎查詢物件屬性時,先查詢物件本身是否存在該屬性,如果不存在,會在物件的
__proto__
裡找,還找不到就會沿著原型鏈一直找到原型鏈頂端(Object.prototype) 直到找到屬性為止,最後在原型鏈頂端都沒找到就返回undefined
。
由於上面的機制,原型的作用就很明顯了——共享屬性,節省記憶體空間。
function Animal() {
this.name = '動物'
this.eat = function() {
console.log('在吃···')
}
}
var a1 = new Animal()
var a2 = new Animal()
console.log(a1.eat === a2.eat) // false
// 每個物件的 eat 方法不是同一個,但方法類容一樣,浪費記憶體
複製程式碼
使用原型解決:
function Animal(name) {
this.name = '動物'
}
Animal.prototype.eat = function() {
console.log('吃')
}
var a1 = new Animal()
var a2 = new Animal()
console.log(a1.eat === a2.eat) //true
// a1.eat 和 a2.eat 都同是一個方法(Animal.prototype.eat)
複製程式碼
原型非常適合封裝共享的方法。但是上面的程式碼把建構函式和原型分開寫了。封裝不到位。使用動態型別模式解決。
function Animal() {
this.name = '動物'
/*
判斷 this.eat 是不是 函式型別,
如果不是,則表示是第一次建立物件或者呼叫 Animal 函式,
會將 eat 新增到原型中去。
如果是,則表示原型中存在了 eat 方法,不需要再新增。
*/
if(typeof this.eat !== 'function') {
Animal.prototype.eat = function() {
console.log('吃')
}
}
}
var a = new Animal()
a.eat()
複製程式碼
原型基於之前的共享屬性和方法,是實現 JS 中繼承的基礎。
與原型有關的方法
hasOwnProperty()
通過之前的學習,我們知道了去訪問一個物件的屬性時,會在原型鏈上查詢。所以我們並不知道這個屬性來自哪裡。
hasOwnProperty()
方法返回一個布林值,可以判斷一個屬性是否來自物件本身。
function Animal() {}
Animal.prototype.name = '動物'
var a = new Animal()
a.age = 3
console.log(a.hasOwnProperty('name')) // false
console.log(a.hasOwnProperty('age') // true
複製程式碼
in
操作符
in
操作符用返回一個布林值,用來判斷一個屬效能否在物件上找到。在物件的原型鏈上找到也返回true
。
function Animal() {}
Animal.prototype.name = '動物'
var a = new Animal()
a.age = 3
console.log('name' in a) // true
console.log('age' in a) // true
console.log('sex' in a) // false
複製程式碼
總結
- 原型就是一個物件,宣告函式就會建立原型物件
prototype
只存在於函式中- 所有物件都有一個
__proto__
屬性,它指向物件的建構函式的原型 - 原型也是物件,也有
__proto__
屬性,__proto__
將物件和原型連線起來,形成原型鏈 Object.prototype
是原型鏈的頂端- 訪問物件的屬性會沿著物件的原型鏈找下去
- 原型可以共享屬性和方法,是繼承的基礎
參考資料: