深入理解Javascript物件導向程式設計

發表於2015-12-23

一:理解建構函式原型(prototype)機制

prototype是javascript實現與管理繼承的一種機制,也是物件導向的設計思想.建構函式的原型儲存著引用物件的一個指標,該指標指向與一個原型物件,物件內部儲存著函式的原始屬性和方法;我們可以藉助prototype屬性,可以訪問原型內部的屬性和方法。

當建構函式被實列化後,所有的例項物件都可以訪問建構函式的原型成員,如果在原型中宣告一個成員,所有的實列方法都可以共享它,比如如下程式碼:

原型具有普通物件結構,可以將任何普通物件設定為原型物件; 一般情況下,物件都繼承與Object,也可以理解Object是所有物件的超類,Object是沒有原型的,而建構函式擁有原型,因此實列化的物件也是Object的實列,如下程式碼:

如上程式碼,Function是Object的實列,也可以是Object也是Function的實列;他們是2個不同的構造器,我們繼續看如下程式碼:

我們明白,在原型上增加成員屬性或者方法的話,它被所有的實列化物件所共享屬性和方法,但是如果實列化物件有和原型相同的成員成員名字的話,那麼它取到的成員是本實列化物件,如果本實列物件中沒有的話,那麼它會到原型中去查詢該成員,如果原型找到就返回,否則的會返回undefined,如下程式碼測試

二:理解原型域鏈的概念

原型的優點是能夠以物件結構為載體,建立大量的實列,這些實列能共享原型中的成員(屬性和方法);同時也可以使用原型實現物件導向中的繼承機制~ 如下程式碼:下面我們來看這個建構函式AA和建構函式BB,當BB.prototype = new AA(11);執行這個的時候,那麼B就繼承與A,B中的原型就有x的屬性值為11

實列化A new A(1)的時候 在A函式內this.x =1, B.prototype = new A(1);B.prototype 是A的實列 也就是B繼承於A, 即B.prototype.x = 1;  如下程式碼:

C.prototype = new B(2); 也就是C.prototype 是B的實列,C繼承於B;那麼new B(2)的時候 在B的建構函式內 this.x = 2;那麼 C的原型上會有一個屬性x =2 即C.prototype.x = 2; 如下程式碼:

下面是實列化 var d = new C(3); 實列化C的建構函式時候,那麼在C的建構函式內this.x = 3; 因此如下列印實列化後的d.x = 3;如下程式碼:

刪除d.x 再訪問d.x的時候 本實列物件被刪掉,只能從原型上去查詢;由於C.prototype = new B(2); 也就是C繼承於B,因此C的原型也有x = 2;即C.prototype.x = 2; 如下程式碼:

刪除C.prototype.x後,我們從上面程式碼知道,C是繼承於B的,自身的原型被刪掉後,會去查詢父元素的原型鏈,因此在B的原型上找到x =1; 如下程式碼:

當刪除B的原型屬性x後,由於B是繼承於A的,因此會從父元素的原型鏈上查詢A原型上是否有x的屬性,如果有的話,就返回,否則看A是否有繼承,沒有繼承的話,繼續往Object上去查詢,如果沒有找到就返回undefined 因此當刪除B的原型x後,delete B.prototype.x; 列印出A上的原型x=0; 如下程式碼:

在javascript中,一切都是物件,Function和Object都是函式的實列;建構函式的父原型指向於Function原型,Function.prototype的父原型指向與Object的原型,Object的父原型也指向與Function原型,Object.prototype是所有原型的頂層;

如下程式碼:

三:理解原型繼承機制

建構函式都有一個指標指向原型,Object.prototype是所有原型物件的頂層,比如如下程式碼:

給Object.prototype 定義一個屬性,通過字面量構建的物件的話,都會從父類那邊獲取Object.prototype的屬性;

從上面程式碼我們知道,原型繼承的方法是:假如A需要繼承於B,那麼A.prototype(A的原型) = new B()(作為B的實列) 即可實現A繼承於B; 因此我們下面可以初始化一個空的建構函式;然後把物件賦值給建構函式的原型,然後返回該建構函式的實列; 即可實現繼承; 如下程式碼:

如上程式碼:我們先檢測Object是否已經有Object.create該方法;如果沒有的話就建立一個; 該方法內建立一個空的構造器,把引數物件傳遞給建構函式的原型,最後返回該建構函式的實列,就實現了繼承方式;如上測試程式碼:先定義一個a物件,有成員屬性name=’longen’,還有一個getName()方法;最後返回該name屬性; 然後定義一個b空物件,使用Object.create(a);把a物件繼承給b物件,因此b物件也有屬性name和成員方法getName();

 理解原型查詢原理:物件查詢先在該建構函式內查詢對應的屬性,如果該物件沒有該屬性的話,

那麼javascript會試著從該原型上去查詢,如果原型物件中也沒有該屬性的話,那麼它們會從原型中的原型去查詢,直到查詢的Object.prototype也沒有該屬性的話,那麼就會返回undefined;因此我們想要僅在該物件內查詢的話,為了提高效能,我們可以使用hasOwnProperty()來判斷該物件內有沒有該屬性,如果有的話,就執行程式碼(使用for-in迴圈查詢):如下:

如上使用for-in迴圈查詢物件裡面的屬性,但是我們需要明白的是:for-in迴圈查詢物件的屬性,它是不保證順序的,for-in迴圈和for迴圈;最本質的區別是:for迴圈是有順序的,for-in迴圈遍歷物件是無序的,因此我們如果需要物件保證順序的話,可以把物件轉換為陣列來,然後再使用for迴圈遍歷即可;

下面我們來談談原型繼承的優點和缺點

B.prototype = new A(1);這句程式碼執行的時候,B的原型繼承於A,因此B.prototype也有A的屬性和方法,即:B.prototype.x1 = 1; B.prototype.getX1 方法;但是B也有自己的特權屬性x2和特權方法getX2; 如下程式碼:

實列化B的時候 b.x1 首先會在建構函式內查詢x1屬性,沒有找到,由於B的原型繼承於A,因此A有x1屬性,因此B.prototype.x1 = 1找到了;var c = new C(3); 實列化C的時候,從上面的程式碼可以看到C繼承於B,B繼承於A,因此在C函式中沒有找到x1屬性,會往原型繼續查詢,直到找到父元素A有x1屬性,因此c.x1 = 1;c.getX3()方法; 返回this.x3+this.x2 this.x3 = 3;this.x2 是B的屬性,因此this.x2 = 2;c.getX2(); 查詢的方法也一樣,不再解釋

prototype的缺點與優點如下:

優點是:能夠允許多個物件實列共享原型物件的成員及方法,

缺點是:1. 每個建構函式只有一個原型,因此不直接支援多重繼承;

2. 不能很好地支援多引數或動態引數的父類。在原型繼承階段,使用者還不能決定以

什麼引數來實列化建構函式。

四:理解使用類繼承(繼承的更好的方案)

類繼承也叫做建構函式繼承,在子類中執行父類的建構函式;實現原理是:可以將一個建構函式A的方法賦值給另一個建構函式B,然後呼叫該方法,使建構函式A在建構函式B內部被執行,這時候建構函式B就擁有了建構函式A中的屬性和方法,這就是使用類繼承實現B繼承與A的基本原理;

如下程式碼實現demo:

上面的程式碼實現了簡單的類繼承的基礎,但是在複雜的程式設計中是不會使用上面的方法的,因為上面的程式碼不夠嚴謹;程式碼的耦合性高;我們可以使用更好的方法如下:

下面我們來分析上面的程式碼:

在建構函式B內,使用A.call(this,x);這句程式碼的含義是:我們都知道使用call或者apply方法可以改變this指標指向,從而可以實現類的繼承,因此在B建構函式內,把x的引數傳遞給A建構函式,並且繼承於建構函式A中的屬性和方法;

使用這句程式碼:B.prototype = new A();  可以實現原型繼承,也就是B可以繼承A中的原型所有的方法;console.log(B.prototype.constructor); 列印出輸出建構函式A,指標指向與建構函式A;我們明白的是,當定義建構函式時候,其原型物件預設是一個Object型別的一個例項,其構造器預設會被設定為建構函式本身,如果改動建構函式prototype屬性值,使其指向於另一個物件的話,那麼新物件就不會擁有原來的constructor的值,比如第一次列印console.log(B.prototype.constructor); 指向於被例項化後的建構函式A,重寫設定B的constructor的屬性值的時候,第二次列印就指向於本身B;因此B繼承與構造A及其原型的所有屬性和方法,當然我們也可以對建構函式B重寫建構函式A中的方法,如上面最後幾句程式碼是對建構函式A中的getX方法進行重寫,來實現自己的業務~;

五:建議使用封裝類實現繼承

封裝類實現繼承的基本原理:先定義一個封裝函式extend;該函式有2個引數,Sub代表子類,Sup代表超類;在函式內,先定義一個空函式F, 用來實現功能中轉,先設定F的原型為超類的原型,然後把空函式的例項傳遞給子類的原型,使用一個空函式的好處是:避免直接例項化超類可能會帶來系統效能問題,比如超類的例項很大的話,例項化會佔用很多記憶體;

如下程式碼:

注意:在封裝函式中,有這麼一句程式碼:Sub.sup = Sup.prototype; 我們現在可以來理解下它的含義:

比如在B繼承與A後,我給B函式的原型再定義一個與A相同的原型相同的方法add();

如下程式碼

那麼B函式中的add方法會覆蓋A函式中的add方法;因此為了不覆蓋A類中的add()方法,且呼叫A函式中的add方法;可以如下編寫程式碼:

B.sup.add.call(this); 中的B.sup就包含了建構函式A函式的指標,因此包含A函式的所有屬性和方法;因此可以呼叫A函式中的add方法;

如上是實現繼承的幾種方式,類繼承和原型繼承,但是這些繼承無法繼承DOM物件,也不支援繼承系統靜態物件,靜態方法等;比如Date物件如下:

如上程式碼執行列印出object,我們可以看到使用類繼承無法實現系統靜態方法date物件的繼承,因為他不是簡單的函式結構,對宣告,賦值和初始化都進行了封裝,因此無法繼承;

下面我們再來看看使用原型繼承date物件;

我們從程式碼中看到,使用原型繼承也無法繼承Date靜態方法;但是我們可以如下封裝程式碼繼承:

六:理解使用複製繼承

複製繼承的基本原理是:先設計一個空物件,然後使用for-in迴圈來遍歷物件的成員,將該物件的成員一個一個複製給新的空物件裡面;這樣就實現了複製繼承了;如下程式碼:

如上程式碼:先定義一個建構函式A,函式裡面有2個屬性x,y,還有一個add方法,該建構函式原型有一個mul方法,首先實列化下A後,再建立一個空物件obj,遍歷物件一個個複製給空物件obj,從上面的列印效果來看,我們可以看到已經實現了複製繼承了;對於複製繼承,我們可以封裝成如下方法來呼叫:

上面封裝的擴充套件繼承方法中的this物件指向於當前實列化後的物件,而不是指向於建構函式本身,因此要使用原型擴充套件成員的話,就需要使用constructor屬性來指向它的構造器,然後通過prototype屬性指向建構函式的原型;

複製繼承有如下優點:

1. 它不能繼承系統核心物件的只讀方法和屬性

2. 如果物件資料非常多的話,這樣一個個複製的話,效能是非常低的;

3. 只有物件被實列化後,才能給遍歷物件的成員和屬性,相對來說不夠靈活;

4. 複製繼承只是簡單的賦值,所以如果賦值的物件是引用型別的物件的話,可能會存在一些副作用;如上我們看到有如上一些缺點,下面我們可以使用clone(克隆的方式)來優化下:

基本思路是:為Function擴充套件一個方法,該方法能夠把引數物件賦值賦值一個空建構函式的原型物件,然後實列化建構函式並返回實列物件,這樣該物件就擁有了該物件的所有成員;程式碼如下:

相關文章