1. 程式人生 > 其它 >總結一下js的原型和原型鏈

總結一下js的原型和原型鏈

最近學習了js的面向物件程式設計,原型和原型鏈這塊是個難點,理解的不是很透徹,這裡蒐集了一些這方面的資料,以備複習所用

一. 原型與建構函式

  Js所有的函式都有一個prototype屬性,這個屬性引用了一個物件,即原型物件,也簡稱原型。這個函式包括建構函式和普通函式,我們講的更多是建構函式的原型,但是也不能否定普通函式也有原型。譬如普通函式:

function F(){
  ;
}
alert(F.prototype instanceof Object) //true

建構函式,也即構造物件。首先了解下通過建構函式例項化物件的過程。

function A(x){
  this.x=x;
}
var obj=new A(1);

例項化obj物件有三步:

  1. 建立obj物件:obj=new Object();

  2. 將obj的內部__proto__指向構造他的函式A的prototype,同時,obj.constructor===A.prototype.constructor(這個是永遠成立的,即使A.prototype不再指向原來的A原型,也就是說:類的例項物件的constructor屬性永遠指向"建構函式"的prototype.constructor),從而使得obj.constructor.prototype指向A.prototype(obj.constructor.prototype===A.prototype,當A.prototype改變時則不成立,下文有遇到)。obj.constructor.prototype與的內部_proto_是兩碼事,例項化物件時用的是_proto_,obj是沒有prototype屬性的,但是有內部的__proto__,通過__proto__來取得原型鏈上的原型屬性和原型方法,FireFox公開了__proto__,可以在FireFox中alert(obj.__proto__);

  3. 將obj作為this去呼叫建構函式A,從而設定成員(即物件屬性和物件方法)並初始化。

  當這3步完成,這個obj物件就與建構函式A再無聯絡,這個時候即使建構函式A再加任何成員,都不再影響已經例項化的obj物件了。此時,obj物件具有了x屬性,同時具有了建構函式A的原型物件的所有成員,當然,此時該原型物件是沒有成員的。

  原型物件初始是空的,也就是沒有一個成員(即原型屬性和原型方法)。可以通過如下方法驗證原型物件具有多少成員。

var num=0;
for(o in A.prototype) {
  alert(o);//alert出原型屬性名字
  num++;
}
alert("member: " + num);//alert出原型所有成員個數。

但是,一旦定義了原型屬性或原型方法,則所有通過該建構函式例項化出來的所有物件,都繼承了這些原型屬性和原型方法,這是通過內部的_proto_鏈來實現的。

  譬如

  A.prototype.say=function(){alert("Hi")};

  那所有的A的物件都具有了say方法,這個原型物件的say方法是唯一的副本給大家共享的,而不是每一個物件都有關於say方法的一個副本。

二. 原型與繼承

  首先,看個簡單的繼承實現。

function A(x){
  this.x=x;
}  4  function B(x,y){
  this.tmpObj=A;
  this.tmpObj(x);
  delete this.tmpObj;
  this.y=y;
}

第5、6、7行:建立臨時屬性tmpObj引用建構函式A,然後在B內部執行,執行完後刪除。當在B內部執行了this.x=x後(這裡的this是B的物件),B當然就擁有了x屬性,當然B的x屬性和A的x屬性兩者是獨立,所以並不能算嚴格的繼承。第5、6、7行有更簡單的實現,就是通過call(apply)方法:A.call(this,x);

這兩種方法都有將this傳遞到A的執行裡,this指向的是B的物件,這就是為什麼不直接A(x)的原因。這種繼承方式即是類繼承(js沒有類,這裡只是指建構函式),雖然繼承了A構造物件的所有屬性方法,但是不能繼承A的原型物件的成員。而要實現這個目的,就是在此基礎上再新增原型繼承。

  通過下面的例子,就能很深入地瞭解原型,以及原型參與實現的完美繼承。(本文核心在此^_^)

function A(x){
   this.x = x;
 }
 A.prototype.a = "a";
 function B(x,y){
   this.y = y;
   A.call(this,x);
 }
 B.prototype.b1 = function(){
   alert("b1");
 }
 B.prototype = new A();
 B.prototype.b2 = function(){
   alert("b2");
 }
 B.prototype.constructor = B;
 var obj = new B(1,3);

這個例子講的就是B繼承A。第7行類繼承:A.call(this.x);上面已講過。實現原型繼承的是第12行:B.prototype = new A();

  就是說把B的原型指向了A的1個例項物件,這個例項物件具有x屬性,為undefined,還具有a屬性,值為"a"。所以B原型也具有了這2個屬性(或者說,B和A建立了原型鏈,B是A的下級)。而因為方才的類繼承,B的例項物件也具有了x屬性,也就是說obj物件有2個同名的x屬性,此時原型屬性x要讓位於例項物件屬性x,所以obj.x是1,而非undefined。第13行又定義了原型方法b2,所以B原型也具有了b2。雖然第9~11行設定了原型方法b1,但是你會發現第12行執行後,B原型不再具有b1方法,也就是obj.b1是undefined。因為第12行使得B原型指向改變,原來具有b1的原型物件被拋棄,自然就沒有b1了。

  第12行執行完後,B原型(B.prototype)指向了A的例項物件,而A的例項物件的構造器是建構函式A,所以B.prototype.constructor就是構造物件A了(換句話說,A構造了B的原型)。

alert(B.prototype.constructor)出來後就是"function A(x){...}" 。同樣地,obj.constructor也是A構造物件,alert(obj.constructor)出來後就是"function A(x){...}" ,也就是說B.prototype.constructor===obj.constructor(true),但是B.prototype===obj.constructor.prototype(false),因為前者是B的原型,具有成員:x,a,b2,後者是A的原型,具有成員:a。如何修正這個問題呢,就在第16行,將B原型的構造器重新指向了B建構函式,那麼B.prototype===obj.constructor.prototype(true),都具有成員:x,a,b2。

  如果沒有第16行,那是不是obj = new B(1,3)會去呼叫A建構函式例項化呢?答案是否定的,你會發現obj.y=3,所以仍然是呼叫的B建構函式例項化的。雖然obj.constructor===A(true),但是對於new B()的行為來說,執行了上面所說的通過建構函式建立例項物件的3個步驟,第一步,建立空物件;第二步,obj.__proto__ === B.prototype,B.prototype是具有x,a,b2成員的,obj.constructor指向了B.prototype.constructor,即建構函式A;第三步,呼叫的建構函式B去設定和初始化成員,具有了屬性x,y。雖然不加16行不影響obj的屬性,但如上一段說,卻影響obj.constructor和obj.constructor.prototype。所以在使用了原型繼承後,要進行修正的操作。

  關於第12、16行,總言之,第12行使得B原型繼承了A的原型物件的所有成員,但是也使得B的例項物件的構造器的原型指向了A原型,所以要通過第16行修正這個缺陷。