1. 程式人生 > 程式設計 >全面解析js中的原型,原型物件,原型鏈

全面解析js中的原型,原型物件,原型鏈

理解原型

我們建立的每一個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。看如下例子:

function Person(){
}
Person.prototype.name = 'ccc'
Person.prototype.age = 18
Person.prototype.sayName = function (){
 console.log(this.name);
}

var person1 = new Person()
person1.sayName() // --> ccc

var person2 = new Person()
person2.sayName() // --> ccc

console.log(person1.sayName === person2.sayName) // --> true

理解原型物件

根據上面程式碼,看下圖:

全面解析js中的原型,原型物件,原型鏈

需要理解三點:

  1. 我們只要建立了一個新的函式,就會根據一組特定的規則為該函式建立一個prototype屬性,指向函式的原型物件。即Person(建構函式)有一個prototype指標,指向Person.prototype
  2. 預設情況下,每個原型物件上都會建立一個constructor(建構函式)屬性,這個屬性是一個指向prototype屬性所在函式的指標
  3. 每個例項的內部都有一個指標(內部屬性) ,指向建構函式的原型物件。即 person1 和person2 身上都有一個內部屬性__proto__(在ECMAscript中管這個指標叫[[prototype]],雖然在指令碼中沒有標準的方式訪問[[prototype]],但是firefox,ie,chrome都支援一個屬性叫__proto__) 指向Person.prototype

注意:person1 和person2 例項與建構函式之間沒有直接的關係。

在之前我們提到,所有實現中無法訪問到[[prototype]],那我們如何知道例項和原型物件之間是否存在關係呢?這裡可以通過兩個方法來判斷:

  • 原型對線上的方法:isPrototypeOf(),如:console.log(Person.prototype.isPrototypeOf(person1)) // --> true
  • ECMAscript5中新增的一個方法:Object.getPrototypeOf(),這個方法返回[[prototype]]的值。如:console.log(Object.getPrototypeOf(person1) === Person.prototype) // --> true

例項屬性與原型屬性的關係

前面我們提到過,原型最初只包含constructor屬性,而該屬性也是共享的,因此可以通過物件例項訪問。雖然可以通過物件例項訪問儲存在原型中的值,但卻不能通過物件例項重寫原型中的值。如果我們在例項中添加了一個屬性,而改屬性與例項原型中的一個屬性同名,那就會在例項上建立該屬性並遮蔽原型中的那個屬性。如下:

function Person() {}
Person.prototype.name = "ccc";
Person.prototype.age = 18;
Person.prototype.sayName = function() {
 console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = 'www' // 在person1中新增一個name屬性
person1.sayName() // --> 'www'————'來自例項'
person2.sayName() // --> 'ccc'————'來自原型'

console.log(person1.hasOwnProperty('name')) // --> true
console.log(person2.hasOwnProperty('name')) // --> false

delete person1.name // --> 刪除person1中新新增的name屬性
person1.sayName() // -->'ccc'————'來自原型'

我們如何判斷一個屬性,到底是例項上的屬性還是原型上的屬性?這裡可以通過hasOwnProperty()方法來檢測一個屬性是存在於例項中還是存在於原型中。(此方法繼承於Object)

下圖詳細分析了上面例子在不同情況下的實現與原型的關係:(省略了Person建構函式的的關係)

全面解析js中的原型,原型鏈

更簡單的原型語法

我們不可能總像之前的例子一樣,沒新增一個屬性和方法就要敲一遍,Person.prototype。為了減少不必要的輸入,更常見的方法是像下面這樣:

function Person(){}
Person.prototype ={
 name: 'ccc',age: 18,sayName: function () {
 console.log(this.name)
 }
}

在上面程式碼中,我們將Person.prototype設定為等於一個以物件字面量形式建立的新物件。最終結果相同,但有一個例外,constructor屬性不再指向Person了。前面我們介紹過,每建立一個函式,就會同時建立它的prototype物件,這個物件也會自動獲得constructor屬性。但是在我們使用的新語法中,本質上完全重寫了預設的prototype物件,因此constructor屬性也就變成了新物件的constructor屬性(指向Object建構函式),不再指向Person函數了。此時,儘管instanceof操作符還能返回正確的結果,但通過constructor已經無法確定物件的型別了。如下:

var person1 = new Person()
console.log(person1 instanceof Object) // --> true
console.log(person1 instanceof Person) // --> true
console.log(person1.constructor === Person) // --> false
console.log(person1.constructor === Object) // --> true

這裡用instanceof操作符測試Object和Person仍然返回true,constructor屬性則等於Object,不等於Person了,如果constructor真的很重要可以像下面這樣寫:

function Person(){}
Person.prototype ={
 constructor: Person,// --> 重設
 name: 'ccc',sayName: function () {
 console.log(this.name)
 }
}

但是這會引起一個新問題,用上述方式重置constructor屬性會導致它的[[Enumerable]]特性被設定為true。而預設情況下,原生的constructor屬性是不可列舉的。因此如果你要使用相容ECMAscript5的JavaScript引擎,可以試一試Object.defineProperty()。

function Person(){}
Person.constructor = {
 name: 'ccc',sayName: function(){
 console.log(this.name)
 }
}
// 重設建構函式,只適用於ECMAscript5相容的瀏覽器
Object.defineProperty(Person.constructor,"constructor",{
 enumerable: false,value: Person
})

原型的動態性

由於原型中查詢值的過程是一次搜尋,因此我們對原型物件所做的任何修改都能立即從例項上反映出來。比如:

function Person(){}
var person1 = new Person()
Person.prototype.sayHi= function(){
 console.log('hi')
}
person1.sayHi()

上述程式碼我們先建立了一個Person例項,並將其儲存在person1中,然後在Person.prototype中添加了sayHi()方法。即使person1是新增新方法之前建立的,但它仍然可以訪問這個方法。原因是例項與原型之間的鬆散的連線關係。
儘管可以隨時為原型新增屬性和方法,並立即能夠在例項中反映出來。但是如果重寫整個原型物件,那麼情況就不一樣了。看如下程式碼:

function Person(){}
var person1 = new Person()

Person.prototype = {
 name: 'ccc',sayName: function(){
 console.log(this.name)
 }
}

person1.sayName() // --> error

看下圖分析:

全面解析js中的原型,原型鏈

呼叫建構函式時為例項添加了一個指向最初原型的[[prototype]]指標,而把原型修改為另外一個物件就等於切斷了建構函式與最初原型之間的聯絡。請記住:例項中的指標僅指向原型,而不指向建構函式。

理解原型鏈

原型鏈是實現繼承的主要方法。其基本思想是讓一個引用型別繼承另一個引用型別的屬性和方法。在理解原型鏈之前,我們首先得捋一下,原型,原型物件,例項之間的關係:每一個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。假如我們讓原型物件等於另一個型別的例項會怎麼樣?顯然,這個原型物件將會包含一個指向另一個原型的指標。先看程式碼在看圖:

function SuperType(){
 this.property = true
}

SuperType.prototype.getSuperValue = function(){
 return this.property
}

function SubType(){
 this.subProperty = false
}

// 繼承了SuperType
SubType.prototype = new SuperType()

SubType.prototype.getSubValue = function (){
 return this.subProperty
}

var instance = new SubType()
console.log(instance.getSuperValue()) // --> true

上述程式碼定義了兩個型別:SuperType和SubType。每個型別分別有一個屬性和一個方法。

全面解析js中的原型,原型鏈

分析上圖:instance 指向SubType原型,SubType的原型又指向SuperType的原型。getSuperValue()方法仍然還在SuperType.prototype中,但property則位於SubType.prototype中。這是因為property是一個例項屬性,而getSuperValue()則是一個原型方法。既然SubType.prototype現在是SuperType的例項,那麼property當然就位於該例項中。此外要注意,instance.constructor現在指向的是SuperType,這是因為原來的SubType.prototype中的constructor被重寫了的緣故。
為什麼會返回true?
分析:呼叫instance.getSuperValue()方法會經歷三個搜尋步驟:

搜尋例項
搜尋SubType.prototype
搜尋SuperType.prototype,直到這裡才找到方法。在找不到屬性或方法的情況下,搜尋過程總是要一環一環地前行到原型鏈末端才會停下來。

別忘記預設的原型

要知道,所有的引用型別預設都繼承了Object,而這個繼承也是通過原型鏈實現的。所有函式的預設原型都是Object的例項,因此預設原型都會包含一個內部指標,指向Object.prototype,這也正是所有自定義型別都會有toString(),valueOf()方法的原因。所以完整的原型鏈應該如下:
看下圖,subType的內部:

全面解析js中的原型,原型鏈

詳細圖解:

全面解析js中的原型,原型鏈

總之一句話,SubType繼承了SuperType,而SuperType繼承了Object。當呼叫instanct.toString()的時候,實際上呼叫的是儲存在Object.prototype中的那個方法。

確定原型和例項的關係

當一個原型鏈很長的時候,想要確定原型和例項的關係,總共有兩種方法:

使用instanceof 操作符,只要用這個操作符來測試例項與原型鏈中出現過的建構函式,結果就會返回true。

console.log(instance instanceof Object) // --> true
console.log(instance instanceof SuperType) // --> true
console.log(instance instanceof SubType) // --> true

使用isPrototypeOf()方法,跟instanctof判別方法類似,只要原型鏈中出現過的原型,都會返回true。

console.log(Object.prototype.isPrototypeOf(instance)) // --> true
console.log(SuperType.prototype.isPrototypeOf(instance)) // --> true
console.log(SubType.prototype.isPrototypeOf(instance)) // --> true

謹慎地定義方法

子型別有時候需要覆蓋超型別中的某個方法,或者需要新增超型別中不存在的某個方法。但不管怎樣,給原型新增方法的程式碼一定要放在替換原型的語句之後。如下:

function SuperType(){
 this.property = true;
}
SuperType.prototype.getSuperValue = function(){
 return this.property
}

function SubType(){
 this.subProperty = false;
}

// 繼承了 SuperType
SubType.prototype = new SuperType()

// 新增新方法
SubType.prototype.getSubValue = function(){
 return this.subProperty
}
// 重寫超型別中的方法
SubType.prototype.getSuperValue = function(){
 return false
}

var instance = new SubType()
console.log(instance.getSuperValue()) // --> false
var instanceSuper = new SuperType()
console.log(instanceSuper.getSuperValue()) // -> true

上述程式碼中,第一個方法getSubValue()被新增到了SubType中。第二個方法getSuperValue()是原型鏈中已經存在的一個方法,但重寫這個方法將會遮蔽原來的那個方法。即當通過SubType的例項呼叫getSuperValue()時,呼叫的就是這個重新定義的方法,但通過SuperType的例項呼叫getSuperValue()時,還會繼續呼叫原來的那個方法。還有一點,在通過原型鏈實現繼承的時候,不能使用物件自變數建立原型方法,因為這樣會重寫原型鏈,導致原型鏈被切斷。

原型鏈的問題

通過原型來實現繼承時,原型實際上會變成另一個型別的例項,於是,原先的例項屬性就變成了現在的原型屬性了,這就會導致屬性被共享。看如下程式碼:

function SuperType(){
 this.colors = ['white','blue']
}

function SubType(){
}

// 繼承了SuperType
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('red')

var instance2 = new SubType()
console.log(instance1.colors) // -->["white","blue","red"]
console.log(instance2.colors) // -->["white","red"]

在建立子型別的例項時,不能向超型別的建構函式中傳遞引數。實際上,應該是沒有辦法在不影響所有物件例項的情況下,給超型別的建構函式傳遞引數。因此,在實踐中很少會單獨使用原型鏈。

以上就是圖解js中的原型,原型鏈的詳細內容,更多關於js中的原型,原型鏈的資料請關注我們其它相關文章!