天天看點

google closure 筆記-類和繼承相關

closure中使用的類是構造函數加原型的組合繼承方式,在構造函數中定義屬性,在原型上定義方法。因為屬性是私有的,方法是公用的。

1,非引用類型執行個體屬性的預設值:

一般來講,當在構造函數中沒有給某一屬性指派的時候,會使用預設值。此處有一個技巧:

給定一個域執行個體屬性同名的類屬性并取預設值,如果在構造函數中沒有給執行個體屬性指派,則根本不用建立此執行個體屬性。預設使用共享的類屬性。(注意是非引用類型才可以)。

看如下例子:

House = function(numberOfBathrooms) { 
if (goog.isDef(numberOfBathrooms)) {
this.numberOfBathrooms_ = numberOfBathrooms; }
House.prototype.numberOfBathrooms_ = 1;
           

注意其中的numberOfBathrooms_,這是一個執行個體屬性而不是一個類屬性,但是如果在構造函數中沒有給定numberOfBathromms的值,則不會建立執行個體屬性,隻會有一個共享的類屬性。這麼做可以節省記憶體,減少構造函數的執記憶體讀寫。

舉例說明這樣做是不存在問題的:

var h1 = new House();

var h2 = new House(1);

var h3 = new House();

此時h1.numberOfBathroom_ == h2.numberOfBathroom_ == h3.numberOfBathroom_ 1。這是顯然的。但是h1.numberOfBathroom_和h3.numberOfBathroom_其實是一個原型鍊上的共享屬性,而h2.numberOfBathroom_是真正的執行個體屬性。

此時就有疑問了:h1和h3共享一個變量,隻讀是沒有問題的,但是寫操作豈不是會互相影響?

答案是不會,沒有任何影響。

h1.numberOfBathroom_ = 2。此時再讀取 h3.numberOfBathroom_仍然是1,因為h1.numberOfBathroom_ = 2實際上是建立了一個h1的執行個體屬性并指派為2,此執行個體屬性隐藏了同名的原型鍊上的類屬性,是以并沒有改變h1.__proto__.numberOfBathroom_的值,當然不會影響到h3的值。

為什麼引用類型不可以?

舉例說明:

House.prototype.room = ["a"];

此時h1.room = h2.room =["a"];

然後執行 h2.room.push("b"),則 h1.room == h2.room == ["a", "b"];

因為是引用類型,是以互相影響了。

但是如果這樣:h2.room = ["a", "b"]則不會影響到h1,因為已經建立了一個h2的執行個體屬性room,原型鍊上的room沒有變。

2,靜态方法的實作

靜态方法就是在類中定義的方法,可以通過類名直接通路。

如下例:

/**
* This would be referred to as an instance method of House because it is * defined on House's prototype.
* @param {example.House} house2
* @return {number}
*/
example.House.prototype.calculateDistance = function(house2) {
return goog.math.Coordinate.distance(this.getLatLng(), house2.getLatLng()); };
/**
* This would be referred to as a static method of House because it is * defined on House's constructor function.
* @param {example.House} house1
* @param {example.House} house2
* @return {number}
*/
example.House.calculateDistance = function(house1, house2) {
return goog.math.Coordinate.distance(house1.getLatLng(), house2.getLatLng()); };
           

第一種是一般用的定義類的函數的方法,所謂“執行個體方法”,因為可以在每一個執行個體中通路。

第二種是靜态方法,因為其定義在類上,這個方法無法在執行個體中直接通路,但是可以通過instantance.constructor.calculateDistance()來通路。

和java的差別:

在java中,靜态方法既可以在類總共通路,也可以在屬性中直接通路。但是在js中不可以,因為靜态方法存在于構造函數上而不是原型上,是以執行個體中無法通過原型鍊來通路此屬性,隻能通過constructor取得構造函數後再通路。

3,繼承機制的實作

goog 繼承機制的實作-構造函數+原型的組合繼承方式:

function(childCtor, parentCtor){
1:         function tempCtor() {};
2:        tempCtor.prototype = parentCtor.prototype;
3:         childCtor.superClass_ = parentCtor.prototype;        
4:         childCtor.prototype = new tempCtor();
5:         childCtor.prototype.constructor = childCtor;
 } 
           

1,2,4:建立一個tempCtor 使其prototype引用父類的prototype,然後将子類的prototype指向tempCtor,使子類繼承父類的方法。這是引用繼承,多個子類的執行個體__proto__引用的是同一個tempCtor。

這個和直接new一個parentCtor的差別就是 它沒有父類中的屬性而隻有方法。

為什麼不直接new一個parentCtor?因為子類的__proto__是共享的,直接new出來的,其屬性就會被所有子類共享,這是不合理的。

3:将子類添加一個superClass_屬性指向父類。

5:将子類的constructor指向自己,否則其指向的是父類的constructor,這樣就無法用construtor來判斷構造函數。

可以通過在子類構造函數中調用父類構造函數call方法來繼承父類的屬性。父類屬性被直接添加到子類的屬性上,而不是添加到子類的__proto__上,因為__proto__是一個被所有子類執行個體共享的引用。

4,goog.base詳解

goog.base用以在子類中友善的通路父類中的方法。因為其通過superClass_屬性可以不斷向上尋找父類。

有兩種用法,一個是在構造函數中調用父類構造函數,一個是在方法中調用父類方法(這個需要指定方法名,但是卻必須和目前方法名相同,原因在下面會講到)。

如果不用goog.base來調用父類方法,也可以用ClassName.call(this, args)來調用,但是對于多層次的繼承,有時候不太清楚此方法究竟是在哪個父類中定義的。

因為用superClass_來查找父類,是以這個方法必須和goog.inherits一起用,否則沒有作用。

源碼解析:

1455 goog.base = function(me, opt_methodName, var_args) {

1456   var caller = arguments.callee.caller;

1457   if (caller.superClass_) {

1458     // This is a constructor. Call the superclass constructor.

1459     return caller.superClass_.constructor.apply(

1460         me, Array.prototype.slice.call(arguments, 1));

1461   }

1462

1463   var args = Array.prototype.slice.call(arguments, 2);

1464   var foundCaller = false;

1465   for (var ctor = me.constructor;

1466        ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) {

1467     if (ctor.prototype[opt_methodName] === caller) {

1468       foundCaller = true;

1469     } else if (foundCaller) {

1470       return ctor.prototype[opt_methodName].apply(me, args);

1471     }

1472   }

1473

1474   // If we did not find the caller in the prototype chain,

1475   // then one of two things happened:

1476   // 1) The caller is an instance method.

1477   // 2) This method was not called by the right caller.

1478   if (me[opt_methodName] === caller) {

1479     return me.constructor.prototype[opt_methodName].apply(me, args);

1480   } else {

1481     throw Error(

1482         'goog.base called from a method of one name ' +

1483         'to a method of a different name');

1484   }

1485 };

分析:

其中的caller就是調用goog.base的那個函數。

首先,如果claller.superClass_存在,說明caller是一個構造函數,其目的是調用父類構造函數,直接調用superClass_即可。

然後,從me.constructor開始向上一級父類查找,如果cons[methodName] === caller(methodName必須和caller的名字是一樣的,因為繼承的原則要求子類中重載父類方法就應該是同名的,如果想調用父類中非同名的方法,直接方法名調用就行了),找到了caller,再向上一級沒有找到,說明存在于cons的原型鍊上,是以可以直接調用。(注:這裡可能會出現一種錯誤見後面注釋)。

如果還沒找到,則可能是在執行個體的方法上調用此執行個體的原型鍊上的方法(例子如下),是以檢測執行個體上caller是否在執行個體上,如果是,則可以調用其原型鍊上的方法。(因為繼承來的方法隻可能在原型鍊上,同理也可能會出現錯誤)。

 13 Human = function(){

 14 }

 15 Human.prototype.say = function(m){

 16     console.log("say:" + m);

 17 }

 18

 19 Boy = function(){

 20     Human.call(this);

 21 }

 22 goog.inherits(Boy, Human);

 23 Boy.prototype.sa = function(m){

 24     console.log(1);

 25 }

 26

 27 var b1 = new Boy();

 28 //var b2 = new Boy();

 29 b1.sa = function(m){

 30     goog.base(this, 'sa', m)

 31 }

 32 b1.sa("aa");

上例中,b1.sa調用的是b1.__proto__.sa。

最後,還沒有找到,說明出錯了,要麼是在a方法中調用b方法(這樣的話cons[methodName]===caller永遠不成立),要麼是類的設計就有問題。

注釋:可能出現一種錯誤

因為在目前cons中根據methodName找到了caller,上一級又找不到,就認為methodName必定存在于cons的原型鍊上,

沒有檢查是否真的存在就直接調用,可能會導緻錯誤。如下例:

 16 Human = function(){

 17     Animal.call(this);

 18 }

 19 goog.inherits(Human, Animal);

 20 Human.prototype.say = function(m){

 21     console.log("say:" + m);

 22 }

 23

 24 Boy = function(){

 25     Human.call(this);

 26 }

 27 goog.inherits(Boy, Human);

 28 Boy.prototype.sa = function(m){

 29     goog.base(this, 'sa', m);

 30 }

 31

 32 var b1 = new Boy();

 33 b1.sa("aa");

sa函數中調用goog.base。goog.base發現在Boy的原型鍊上确實能找到sa,而且在Human中确實沒有找到sa,是以直接調用Human的sa函數,實際上human中壓根就沒定義sa,是以出錯。當然,一般情況下是不會出現此錯誤的,出現這種錯誤也沒有意義,因為原因是沒有按照類的繼承規範來做。

5,Disposable:可銷毀的對象

Disposable定義了一個可銷毀對象的基本方法,如果一個對象中包含其他引用:dom,com等,那麼就應該繼承此類,或者實作IDisposable接口。

實作disposeInternal的步驟:

1. Call the superclass’s disposeInternal method.

2. Dispose of all Disposable objects owned by the class. 

3. Remove listeners added by the class.

4. Remove references to DOM nodes.

5. Remove references to COM objects.

參見一個例子:

example.AutoSave.prototype.disposeInternal = function() {

// (1) Call the superclass's disposeInternal() method. 

example.AutoSave.superClass_.disposeInternal.call(this);

// (2) Dispose of all Disposable objects owned by 

this class. goog.dispose(this.failureDialog);

// (3) Remove listeners added by this class. 

this.container.removeEventListener('mouseover', this.eventListener, false);

// (4) Remove references to COM objects. 

this.xhr = null;

// (5) Remove references to DOM nodes, which are COM objects in IE. delete this.container;

this.label = null;

};

源碼解析:

在idisposable.js中定義了IDisposable接口,其隻有兩個方法:dispose和isDisposed。

在disposable.js中定義了Disposable類。

Disposable類分為兩部分,一部分是Disposable執行個體的屬性,一部分是Disposable類的靜态屬性和方法(暫時把它當做一個類,其實還是一個執行個體)。

1,Disposable類的靜态屬性: 管理了一個instances_對象(當做一個map),其中儲存了所有的未被釋放的disposable執行個體的引用。并提供了三個方法來管理這個map:

在構造函數Disposable中會自動把new出來的執行個體添加到instantces_數組中。

getUndisposedObject:取得未被銷毀的所有執行個體,此函數簡單的把instances_轉換成了array類型并傳回。

clearUndisposedObject:清空instances_。

但是,隻有當Disposable.ENABLE_MONITORING == true,時,才instances_才可用,否則,instances_始終為空,因為在構造函數中會檢查ENABLE_MONITORING,隻有true時才将new出來的對象添加到instances_中。預設ENABLE_MONITORING是false的。這個隻是給測試用的,因為會消耗性能。

2,Disposable執行個體:通過dependentDisposables_數組來儲存目前執行個體中包含的所有引用執行個體。并提供了如下方法來管理:

isDisposed:目前執行個體是否已經被銷毀。

dispose:銷毀目前執行個體中包含的所有引用類型。通過調用disposeInternal來實作,而disposeInternal需要在子類中重寫。其預設實作會銷毀所有儲存在dependentDisposables_中的對象,通過調用其中每一個對象的dispose方法來實作。

registerDispose:向dependentDisposables_中添加一個對象。

6,抽象方法

子類中必須重載的方法,否則就會報錯。隻需要将此方法指派為goog.abstractMethod即可。包含抽象方法的類就是“抽象類”,(注意,在原生的js中并沒有抽象方法和抽象類的概念)。

看下其實作:

goog.abstractMethod = function(){

     throw Error("unimplemented abstract method");

}

是以沒有在子類中重載就會報錯。

7,多重繼承

js本身就沒有真正的類和繼承,更别談多重繼承了。closure本身不支援多重繼承,現在也沒有方法可以完美的實作多重繼承。原因是:goog.inherits實際上是把目前類的prototype指向了一個新的類(這個類的prototype指向被繼承的類的prototype),是以第二次使用goog.inherits會覆寫掉第一次繼承的原型方法,是以不支援多重繼承。

但是可以通過goog.mixin方法把其他類的prototype上的屬性全部copy過來,可以達到多重繼承的一部分目的。

如下例:

goog.provide('example.Phone'); goog.provide('example.Mp3Player'); goog.provide('example.AndroidPhone');
/** @constructor */
example.Phone = function(phoneNumber) { /* ... */ };
example.Phone.prototype.makePhoneCall = function(phoneNumber) { /* ... */ };
/** @constructor */
example.Mp3Player = function(storageSize) { /* ... */ };
example.Mp3Player.prototype.playSong = function(fileName) { var mp3 = this.loadMp3FromFile(fileName);
mp3.play();
return mp3;
};
/**
* @constructor
* @extends {example.Phone} */
example.AndroidPhone = function(phoneNumber, storageSize) { example.Phone.call(this, phoneNumber);
example.Mp3Player.call(this, storageSize); };
goog.inherits(example.AndroidPhone, example.Phone); 
goog.mixin(example.AndroidPhone.prototype, example.Mp3Player.prototype);
var p = new example.AndroidPhone();
 console.log(p instanceof example.AndroidPhone);//(1)
 console.log(p instanceof example.Mp3Player);//(2)
 console.log(p instanceof example.Phone);//(3)
           

這種方式本質上是單繼承,隻是調用了其他類的構造函數并copy了prototype上的方法。隻有Phone是真正的被繼承的父類,而Mp3Player隻是被調用了構造函數和copy了prototype上的方法。可以把這個多重繼承的父類分為兩部分:

一種是真正的父類,即Phone,另一種是多重繼承的父類,即用goog.mixin加入的Mp3Player。其中第一類沒有任何問題,但是第二類其實從文法上就不是父類,是以有兩個主要缺點:

1,不能使用instanceof來測試第二類,即上例中的(2)會傳回false。

2,無法用superClass_或者goog.base來通路父類中的方法,隻能記住每個方法到底是在那個類中,然後用類名來調用,這樣當繼承樹複雜時是很麻煩的。

第一個缺點由于js語言的問題,無法解決。但是第二個是可以解決的。一種解決方法如下:

example.mp3.playSongFromFile = function(fileName) {

// This implies that both Mp3Player and AndroidPhone have methods named // getPlayer() that return an object with a playFile() method. this.getPlayer().playFile(fileName);

};

example.Mp3Player.prototype.playSong = example.mp3.playSongFromFile; example.AndroidPhone.prototype.playSong = example.mp3.playSongFromFile;

即建立一個執行個體方法,然後把所有的父類上的同名方法都指向此方法。