javascript中繼承(一)-----原型鏈繼承的個人理解

【寒暄】好久沒有更新博客了,說來話長,因爲我下定決心要從一個後臺程序員轉爲Front End,其間走過了一段漫長而艱辛的時光,今天跟大家分享下自己對javascript中原型鏈繼承的理解。

總的說來,js中的常用的繼承方式可以分爲兩種,一種是原型鏈式繼承,這也是本文要談的重點;另外一種是借用構造函數繼承,這方面的理解,我將在下次的博客中更新。好了,閒話不多說,進入正題。

一,關於原型

首先,我們不得不解釋下原型的概念:我們創建的每一個函數都有一個原型屬性,即prototype,這個屬性是一個指針,指向原型對象。
function Person(){}//這裏我們聲明一個函數Person,js中函數是對象,也是構造函數
console.log(Person.prototype)//打印一下Person對象的原型,會出現什麼呢?如下圖所示:


大家在圖中看到了,Person對象的原型擁有一個constructor,它指向Person的構造函數,即Person本身,另外一個屬性是__proto__屬性,這個屬性我會在後文中說明。

到這裏,大家肯定會明白了,一個對象建立後,會產生一個局部的“小鏈式結構”,即Person對象擁有一個prototype屬性,這個屬性指向原型對象,在原型對象中又有一個構造器constructor,指向構造函數。用一張圖來說明:



那麼,原型對象的作用是什麼呢?這個原型對象包含由特定類型的實例共享的屬性和方法。大家要注意共享這兩個字,用一段代碼解釋下

function Person(){
  this.name="bob" //這是一個實例屬性
}
Person.prototype.eat=function(){ //給對象的原型對象添加一個eat的方法,接下來,new的實例會共享這個方法
  return "food";
}
var p1=new Person();  //這裏究竟發生生了什麼?
p1.eat()//->food
var p2=new Person();
p2.eat()//->food,所以只要是Person的對象,他們都會共享原型對象的方法,當然,p1.name也會共享Person的實例屬性,因爲p1是Person的一個實例  

好了,到這裏原型的概念我們已經講完了,大家或許會疑問,上面的new一個Person實例的過程中究竟發生了什麼呢?爲什麼這個實例能夠訪問到原型對象中的方法?其實,在這個過程過程中,p1實例擁有了一個指針,這個指針指向構造函數的原型對象。此時原型對象中的方法自然能夠被實例所訪問。用一張圖來說明下:

這裏,我們總結下構造函數、原型和實例的關係:每個構造函數都有一個原型對象,原型對象擁有一個指向構造函數的指針,而實例擁有一個指向原型對象的內部指針(這就是前面所提到的[[Prototype]],即__proto__,要注意的是這個__proto__屬性在chrome瀏覽器中是可以看到的,而在大部分瀏覽器是隱藏的!)

二,關於原型鏈繼承

好了,說了這麼多終於到回到我們的主角了【原型鏈】,提出一個思考:如果我們讓原型對象等於另外一個對象的實例,將會有一個什麼樣的結果呢?先看下面一段代碼

function Person(){
  this.name="bob";  
}
Person.prototype.eat=function(){
  return "food";
}
function Student(){}
Student.prototype=new Person();//將Person實例賦給Student的原型對象
var one=new Student();
one.name//bob
one.eat()//food,Student的實例能訪問到Person對象的實例方法,也能訪問到其原型屬性中的方法

以上就是原型鏈繼承的一種基本模式,那麼我們怎麼解釋這樣的原理呢?之前說過,對象的實例擁有一個指向原型對象的指針,那麼student的原型對象擁有了Person對象實例後,自然也擁有一個指向Person原型對象的指針。此時,我們再new一個Student實例one時,one實例包含一個指向Student原型的指針,而Student.prototype擁有一個指向Person原型對象的指針,Person原型本身包含一個指向自身構造函數的指針。這樣一來,就構成了實例與原型的鏈條。這就是所謂的原型鏈的概念!

用一張圖描繪一下上面講的情況:

大家注意一下,這裏的one對象的constructor現在指向誰呢?它並不指向Student,因爲Student的原型指向另一個對象--Person的原型,而這個原型對象的constructor指向的是Person。

三,原型鏈方法的改寫及注意的問題

有時候,子類型需要改寫超類型當中的方法,或者添加新的方法,一定要注意給原型添加代碼一定要放在繼承語句(即替換原型語句)的後面,
function Person(){
  this.name="bob" ;
}
Person.prototype.eat=function(){
  return "food";
}
function Student(){}
//Student.prototype.eat=function(){ 
//  return "food1";       
//}           
//注意如果更改原型語句的代碼放在替換之前,那麼下面one.eat()的結果將仍然是food
//,原因很簡單,前面對prototype對象的修改,在後面的替換一句中被Person實例對象覆蓋了
//,換句話說,就是現在的prototype實例中仍舊是以前的eat方法
Student.prototype=new Person();
Student.prototype.eat=function(){
  return "food1";
}
var one=new Student();
console.log(one.eat());//food1
但是大家要注意一下一種情況,在通過原型鏈繼承時,不能通過對象字面量個方式來更新原型對象

function Person(){
  this.name="bob" ;
}
Person.prototype.eat=function(){
  return "food";
}
function Student(){}

Student.prototype=new Person();
Student.prototype={
  run:function(){
    return "run";
  }
};
var one=new Student();
console.log(one.eat());//Uncaught TypeError: undefined is not a function 
在上面的代碼中,把Person的實例賦給Student的原型,接下來又把原型改寫成另一個對象字面量,現在原型包含的是Object實例,不再是Person實例,因此原型鏈已經被切斷了,也就是說Student和Person沒關係了。

但是思考下面一段代碼,我這樣改寫,會切斷原型鏈嗎?
function Person(){
  this.name="bob" ;
}
Person.prototype.eat=function(){
  return "food";
}
function Student(){}

Student.prototype=new Person();
Student.prototype.constructor=Student;//把Student原型對象中原本指向Person構造函數的對象強行指向到Student
var one=new Student();
console.log(one.eat());//food
從代碼運行的情況來看,這個動作並沒有切斷原型鏈的繼承,原因何在?
大家一定要主要實例和原型對象是通過[[Prototype]]來實現關係鏈接的,換句話說,實例裏面的[[Prototype]]指針指向原型對象,這裏我改寫了constructor後,並沒有將[[Prototype]]的指向改變,當然也就沒有改變整個原型鏈的繼承關係!這一點要非常注意!!!

四,如何確定原型和實例關係

可以通過兩種方式來確定原型和實例之間的關係,第一種是instanceof操作符,這個操作符用來測試實例與原型鏈中出現過的構造函數,看下面的一段代碼
function Person(){
  this.name="bob" ;
}
Person.prototype.eat=function(){
  return "food";
}
function Student(){}

Student.prototype=new Person();
Student.prototype.constructor=Student;
var one=new Student();
var person=new Person();
console.log(one instanceof Student);//true
console.log(one instanceof Person);//true
console.log(person instanceof Person);//true
console.log(person instanceof Student);//false
最後一個出現了false,什麼原因,instanceof的工作是什麼呢?
A intanceof B,它的工作原理是,檢測對象B的prototype指向的對象是否出現在Ad對象的[[Prototype]]鏈上,換句話說,A對象的原型鏈繼承線路上,有沒有B的存在。最後一個例子中Student的原型對象指向Person的原型對象,person實例也是指向Person的原型對象,而Person的原型對象指向了Person的構造函數本身,所以這條person對象的原型鏈路上,並沒有出現Student的原型對象,故最後一個person不是Student的實例!

那麼,第二種檢測方式是isPrototypeOf()方法,同理,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型。
function Person(){
  this.name="bob" ;
}
Person.prototype.eat=function(){
  return "food";
}
function Student(){}

Student.prototype=new Person();

var one=new Student();
var person=new Person();
console.log(Student.prototype.isPrototypeOf(one));//true
console.log(Person.prototype.isPrototypeOf(one));//true
console.log(Person.prototype.isPrototypeOf(person));//true
console.log(Student.prototype.isPrototypeOf(person));//false

五,總結

以上就是原型連接繼承及需要注意的問題,原型鏈繼承固然很強大,但是也有一些問題,比如共享的原型屬性容易被修改,在創建子類型的實例時,不能向超類傳參數,等等,在下一次的分享中,我將談一下自己學習‘借用構造函數實現繼承’的心得!
【感謝】:本人是剛學習前端的菜鳥,以上都是自己的有些學習體會,講的囉嗦和不對的地方,大家可以給我留言,要知道這篇博文我整整畫了一個晚上的時間完成,希望能給那些正在學習js的同學一些參考。感謝《js高級程序設計》,以及《javascript類型檢測》這篇文章!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章