JavaScript系列之原型與原型鏈

米淇淋發表於2019-04-28

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 的值:

JavaScript系列之原型與原型鏈

其實,函式的 prototype 屬性指向了一個物件,這個物件正是呼叫該建構函式而建立的例項的原型

那什麼是原型呢?你可以這樣理解:每一個JavaScript物件(null除外)在建立的時候就會與之關聯另一個物件,這個物件就是我們所說的原型,每一個物件都會從原型"繼承"屬性。

讓我們用一張圖來表示建構函式和例項原型之間的關係:

JavaScript系列之原型與原型鏈

那麼我們該怎麼表示例項與例項原型,也就是 dogDog.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指向同一個物件。於是我們更新下關係圖:

JavaScript系列之原型與原型鏈

既然例項物件和建構函式都可以指向原型,那麼原型是否有屬性指向建構函式或者例項呢?

constructor

指向例項物件倒是沒有,因為一個建構函式可以生成多個例項,但是原型指向建構函式倒是有的,這就要講到第三個屬性:constructor,每個原型都有一個 constructor 屬性指向關聯的建構函式。

為了驗證這一點,我們在chrome中輸入:

function Dog() {
    this.name = '阿黃'
}

console.log(Dog.prototype.constructor === Dog)    // true
複製程式碼

所以再更新下關係圖:

JavaScript系列之原型與原型鏈

綜上我們已經得出:

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
複製程式碼

原型鏈

在上文我們理解了原型,從字面意思看原型鏈肯定是與原型有關了,是一個個原型連結起來的麼?我們先通過下面的圖來觀察一下。

JavaScript系列之原型與原型鏈

解析:

obj.prop1:假設我們現在有一個物件,就稱作obj,而這個物件包含一個屬性(property),我們稱作prop1,現在我們可以使用obj.prop1來讀取這個屬性的值,就可以直接讀取到prop1的屬性值了。

obj.prop2:JavaScript中會有一些預設的屬性和方法,所有的物件和函式都包含prototype這個屬性,假設我們把prototype叫做proto,這時候如果我們使用obj.prop2的時候,JavaScript引擎會先在obj這個物件的屬性裡去尋找有沒有叫作prop2的屬性,如果它找不到,這時候它就會再進一步往該物件的proto裡面去尋找。所以,雖然我們輸入obj.prop2的時候會得到回傳值,但實際上這不是obj裡面直接的屬性名稱,而是在objproto裡面找到的屬性名稱(即,obj.proto.prop2,但我們不需要這樣打)。

obj.prop3:同樣地,每一個物件裡面都包含一個prototype,包括物件proto本身也不例外,所以,如果輸入obj.prop3時,JavaScript會先在obj這個物件裡去尋找有沒有prop3這個屬性名稱,找不到時會再往objproto去尋找,如果還是找不到時,就再往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 ( ) ) ;
複製程式碼

以上就是目前能總結的全部了,肯定還是有缺陷的地方,後續還會修改完善的。最後再看底下這張圖,是否有了更深入的理解呢?

JavaScript系列之原型與原型鏈

如果覺得文章對你有些許幫助,歡迎在我的GitHub部落格點贊和關注,感激不盡!

相關文章