上次講了一下在使用JavaScript進行物件導向編程中,採用構造法來實作類別繼承的一些優缺點。下面我們接著把'原型繼承法'的也有缺點也講一講,希望大家能積極提意見並探討其中的一些問題。
原型(prototype)是JavaScript實現物件導向編程的一個基礎,但它並不是唯一的構造類的方法,我們完全可以不使用prototype而實作類別的編寫(把屬性和方法的附加全都寫在建構函式裡面就行了)。不過原型除了可以為Object的子類添加新的屬性和方法外,它還可以為指令碼環境中的內部對象繼續添加原型屬性和方法,比如我們最常用的給內部對象String添加一個Trim方法來刪除字串兩端的空格,代碼為:
String.prototype.Trim = function()
{
return this.replace(/(^\s*)|(\s*$)/g, '');
}
這樣我們就可以在任何的String執行個體中使用Trim方法了,用慣了這種原型系統,有的時候反而還覺得傳統OOP沒有它爽,kaka。言歸正傳,繼續講我們的原型繼承法。
原型繼承法的原理:
原型繼承法的關鍵代碼是其建構函式function ArrayList02()下的第一句:
ArrayList02.prototype = new CollectionBase();
ArrayList02.prototype.constructor = ArrayList02;
Ae… 把prototype都覆蓋成基類的一個執行個體了,子類還怎麼使用prototype呢?這裡不要著急,反正JavaScript的對象執行個體都是可以動態增刪屬性和方法的,基類執行個體作為prototype不就正好等於extends了CollectionBase嗎?之後再使用XXX.prototype.xxx = function(),可以繼續獲得新增了屬性和方法的對象。注意ArrayList02.prototype是在ArrayList02的建構函式外被初始化為基類的執行個體的。
再來看第二句,為什麼要把ArrayList02賦值給新prototype的constructor呢?如果不做這個賦值,當我們從ArrayList02的執行個體中,通過???.prototype.constructor去卻其建構函式,將會獲得CollectionBase。這不是我們的本意,而且這是使用instanceof關鍵之比較對象執行個體和對象也會出錯。
原型繼承法的缺陷:
原型繼承法有兩個缺陷,第一個是由於類的原型(prototype)實際上是一個Object的執行個體,它不能再次被執行個體化(它的初始化在指令碼裝載時已經執行完畢)。這麼意思呢?我們知道在建立對象執行個體時,我們使用語句new ArrayList02(),這時我們可以看做JavaScript指令碼引擎把prototype的一個淺拷貝作為this返回給類的執行個體(這裡其實沒有發生拷貝,只是利用淺拷貝這個概念來協助理解),如果類沒有附加任何原型屬性和原型方法,那麼就等於返回了一個new Object()執行個體。問題就出在這裡了,由於new對prototype執行的是淺拷貝,如果prototype的原型屬性裡有物件類型的屬性,那麼就會造成共用對象執行個體的問題(類似於在傳統OOP的類定義中使用了static修飾符來修飾了屬性)。這個缺陷下面會有樣本示範,避免的辦法就是不要在基類中定義物件類型的屬性,比如: Array、Object和Date等。
第二個缺陷和上次講的"構造繼承法"的缺陷差不多,也是關於子類定義是語句順序的。就是說代碼:ArrayList02.prototype = new CollectionBase();必須在所有prototype定義之前執行,很簡單,如果在原型屬性和方法都匯入完成後,再執行這個語句,就定於把之前的匯入全都覆蓋掉了:(。解決辦法就是按我給文章(1)中的那個順序來寫,鬱悶吧?kaka
原型繼承法的樣本:
<script language="JavaScript">
document.write('原形繼承法:<br>');
var arrayList21 = new ArrayList02();
arrayList21.Add('a');
arrayList21.Add('b');
arrayList21.foo();
var arrayList22 = new ArrayList02();
arrayList22.Add('a');
arrayList22.Add('b');
arrayList22.Add('c');
arrayList22.foo();
</script>
樣本運行結果為:
原形繼承法:
[class ArrayList02]: 2: a,b
[class ArrayList02]: 3: a,b,c,a,b
發現問題了吧?執行個體arrayList22的foo()居然輸出了a,b,c,a,b@_@... 這就是前面說的prototype對象淺拷貝帶來的問題。不過為什麼arrayList22中的集合計數器卻仍然是3呢?這是因為this.m_Count是整數類型,這種類型又叫實值型別(和C#裡的實值型別、物件類型概念一樣的)。實值型別不存在淺拷貝和深拷貝的問題,可以看成都是深拷貝。
小結:JavaScript的原型繼承法,雖然有prototype淺拷貝那麼嚴重的bug,不過它卻是使用比較多的繼承方式。因為我們很少在基類裡定義屬性,更別說物件類型的屬性了,所以引發這個錯誤的可能性不是很大,只是它是個潛在的隱患:(。至於第二個缺陷,也是一個潛在bug,只要自己定義類的時候能比較清醒就不會犯錯誤。'重載'也比較簡單,如果在ArrayList02.prototype = new CollectionBase();後有重名的屬性或方法匯入,自動覆蓋基類中的屬性或方法就像當於重載了。
應用情境:基類沒有屬性,至少是要沒有物件類型的屬性。這種繼承的優點是保持了子類建構函式的完整,可以不在裡面添加任何和繼承有關係的代碼,所有繼承和重載操作都由對原型(prototype)的操作來完成。
to be continued ...