在Java中,我們在實現繼承的時候存在下面幾個事實:
1, 準備兩個類,他們用extends關鍵字鏈接起來
2, 如果超類沒有默認構造函數,需要在子類構造函數中顯式的super並傳參,如果都是默認構造函數也可以super,不super虛擬機是自動的
3, 子類可追加,覆蓋,重載方法,子類可以有自己的私有屬性,他們在子類構造函數中被構造
4, 字段是數據,方法在代碼區,和類建立方法表,同一個類的對象有自己的數據但是共享方法代碼
比如有兩個類,Plane和Space,Plane表示平面,Space表示空間,Space是Plane的子類,在java中
* 根據字段數量分配內存塊
* 實例化的時候虛擬機調用Plane.Plane方法把這個內存塊作爲this和構造參數傳進去,初始化完數據字段。
* 建立方法表映射
*/
class Plane {
protected int x;
protected int y;
Plane(int x, int y) {
this.x = x;
this.y = y;
}
public void XY() {
System.out.println(x * y);
}
}
/**
* 自動擁有了超類的行爲,但是超類的屬性需要超類去構造
* 子類可構造自己的屬性,添加自己的方法,覆蓋超類的方法
* <p/>
* 按照繼承結構的所有字段分配內存塊,調用Space.Space將這個內存塊作爲this和參數一起傳進去
* 把超類的那部分給超類,然後自己初始化自己的。
* <p/>
* 建立方法表
*/
class Space extends Plane {
private int z;
Space(int x, int y, int z) {
super(x, y);
this.z = z;
}
public void XYZ() {
System.out.println(x * y * z);
}
}
public class Test {
public static void main(String[] args) {
Plane plane = new Plane(2,3);
plane.XY();
Space space = new Space(2, 3, 4);
space.XYZ();
}
}
那麼在js中也一樣,區別是代碼要放到構造函數(可以理解爲Java中的類)的原型上,原型是放置方法和不變屬性的理想場所,原型是一個對象,它和普通對象唯一不同的就是他有一個constructor屬性指向它所依附的構造器,在java中子類查找屬性和方法是通過虛擬機來完成,但是在js中需要通過原型鏈來完成。也就是說繼承關係對程序員是不透明的,需要了解這個原型機制,原型機制上存在兩條鏈,一是原型鏈,二是構造函數鏈。
仿照上面java的代碼,我們可以完成js的寫法,如下:
this.x = x;
this.y = y;
};
Plane.prototype.XY = function() {
alert(this.x * this.y);
};
var Space = function(x, y, z) {
//用this調用超類構造函數,沒有java的super自動調用,所以要手動調用
Plane.call(this, x, y);
//Space.superclass.constructor.call(this, x, y); 可以用一個統一的語法
//構造自己的數據
this.z = z;
};
Space.prototype.XYZ = function() {
alert(this.x * this.y * this.z);
}
JS中函數的this指函數的調用者,不管是java還是js,this都可理解爲新分配的那段容納對象的內存。在java 中通過Space
extends Plane,虛擬機就維護好了他們的繼承關係以完成繼承關係的自動查找,但是在js中需要我們手動的處理,在這個時候Space是調用不到XY這個方法的,因爲他們沒有在原型鏈上。我們可以開發一個函數來模擬java的關鍵字extends,比如這個函數叫做extend,通過執行extend(Plane,Space)完成原型鏈的組裝。
那麼extend怎麼實現呢?首先要明白原型鏈,子類和父類在原型鏈上的關係是Space.prototype._proto_ == Plane.prototype,如果你理解不了,那就看String這個類吧,String.prototype._proto_ == Object.prototype,即String的原型會鏈接到Object的原型上,鏈接是通過_proto_這個屬性來完成的。_proto_是一個只讀的屬性,只能通過構造函數寫入,所以String是Object的子類。
現在Plane的prototype._proto_
等於Object,Space的prototype._proto_也等於Object,我們要在extend函數變換這個關係,即完成Space.prototype._proto_
== Plane.prototype,我們知道一個對象的_proto_要指向某個構造函數的原型,需要讓這個對象由那個構造函數構造,那麼我們只需要讓Space.prototype
= new Plane()就可以了,這個時候Space.prototype._proto_ == Plane.prototype,而不再指向Object,原型還有一個屬性constructor指向原型所在的構造器,由於Space.prototype剛被Plane創建出來,還沒有這個屬性,我們要手動賦值上去,代碼是Space.prototype.
constructor = Space。這樣extend的責任就完成了。
但是這裏有兩個問題:
1, 由於Space的原型在extend中被替換了,那麼它原有的方法就沒有了。
2, Space的原型是Plane構造的,雖然做到了Space.prototype._proto_ == Plane.prototype,但是Plane也在原型上寫入了x,y這兩個垃圾數據,他們都是undefined,沒有意義,所以要手動刪除掉,這樣extend這個方法就不能通用了。
首先解決第一個問題,我們要變化一點思路,利用js中函數也是數據的特性,我們把Space的那些方法拷貝到一個對象中,比如
var sbm= { XYZ : function() {
alert(this.x * this.y * this.z);
}
};
把這個sbm也傳遞給extend,extend在替換完原型後將sbm上的所有方法複製到Space的原型上即可,sbm是一個對象直接量,用json語法。現在的extend就變爲了三個參數,即extend(sb,sp,sbm),sb是子類,sp是超類,sbm是子類要放到原型上的方法。
對於第二個問題,本質原因是Plane這個函數要完成一些數據初始化,它是超類,我們不能控制,我們只關心Plane的原型而不關心它構造什麼數據,所以我們可以把它的原型單獨拿出來,再定義一個乾淨的函數,這個函數不做任何事,將這個乾淨函數的原型設置爲Plane的原型,再用這個乾淨函數構造一個對象,這樣出來的對象就是是乾淨的,也完成了_proto_指向了Plane.prototype,完美!有了這兩個方法,我們就可以開始實現這個extend,代碼如下:
var F = function() {
},sbp,spp = sp.prototype;
F.prototype = spp;
//用乾淨函數嫁接得到子類原型
sbp = sb.prototype = new F();
sbp.constructor = sb; //然後指定一個constructor指回子類
//把sbm的上的屬性拷貝到子類的原型上
for (var p in sbm) {
sbp[p] = sbm[p];
}
};
那麼完成Space繼承Plane的代碼如下:
XYZ : function() {
alert(this.x * this.y * this.z);
}
});
var spaceObject = new Space(2, 3, 4);
spaceObject.XY();//成功調用超類方法
spaceObject.XYZ();
OK,到了這裏,我們基本上就完成任務了,完全從java的方向搞定的。我們現在利用js的特性來優化,讓使用extend更加簡單。
我們說在java中必須寫兩個類,每個類都寫自己的字段 ,構造函數,方法等,在我們實現的extend函數中也確實把子類,父類都傳遞了進來,但是我們多了一個參數,那就是子類的方法集合即sbm,第一個參數sb本身也是函數,那是不是可以將這個函數也放進sbm傳進來呢?這樣extend就變爲兩個參數,即extend(sp,sbm),現在extend返回一個函數,返回的這個函數就是sp的子類,這是完全可行的,我們叫做extend2吧。
var sb = sbm.constructor;
//如果說沒有顯式的構造函數,那麼子類就是直接調用超類構造函數
if (sb == Object) {
sb = function() {
sp.apply(this, arguments);
};
}
extend(sb, sp, sbm);
return sb;
}
我們說要把子類的構造函數放到sbm上,放上去的key叫做constructor,就表示構造器,js中每一個對象都一個constructor屬性,它指向構造了這個對象構造函數。sbm本來是個Object對象,它的constructor就指向Object,這個constructor是在sbm關聯的那個原型上的,現在我們在sbm上設置某個子類的構造函數,這個時候表示sbm有個自己的constructor。
現在我們在extend2中要做的事情就是提取出構造函數,然後還原爲三個參數去調用之前的extend,在java中我們的子類是可以不用構造器的,只要父類也有默認的構造器,那麼在這裏一樣,sbm可能不包含constructor,那麼我們需要做一個函數,它調用父類的構造函數,在java中這種情況過程是自動的。所以當sbm.constructor爲Object的時候表示sbm沒有指定構造函數,這個時候將
sb = function() {
sp.apply(this, arguments);
};
即調用父類構造函數。這樣將sb,sp,sbm傳遞給extend就可以了。
這個時候我們新的繼承語法如下:
constructor : function(x, y, z) {
Plane.call(this, x, y);
this.z = z;
},
XYZ : function() {
alert(this.x * this.y * this.z);
}
});
var newObject = new NewSpace(3, 4, 5);
newObject.XY();
newObject.XYZ();
和prototype.js和mootolls的實現比較,大同小異
var Person = Class.create({
initialize: function(name) {
this.name = name;
},
say: function(message) {
return this.name + ': ' + message;
}
});
initialize: function(age) {
this.age = age;
},
say : function() {
alert(this.age);
}
});
var Cat = new Class({
Extends: Animal,
initialize: function(name, age) {
this.parent(age); // calls initalize method of Animal class
this.name = name;
}
});
現在我們來看看Ext.extend,應該完全沒有問題了。我們用了兩個方法extend,extend2,Ext把它合併爲了一個方法Ext.extend,所以它會判斷傳進來的參數然後進行變換,這樣Ext.extend就支持兩個參數和三個參數進行調用。對於前面用到拷貝屬性,Ext做了一個工具函數叫做Ext.apply,對於將一個對象的屬性拷貝到一個類的原型上,Ext做了一個工具類叫做Ext.override。
// inline overrides 把傳入的對象屬性複製到到this中
var io = function(o) {
for (var m in o) {
this[m] = o[m];
}
};
//oc其實就是Object函數
var oc = Object.prototype.constructor;
return function(sb, sp, overrides) {
//如果第二個參數是個對象而不是類,那麼是用兩個參數調用的,第一個參數是父類,第二個參數是對象
if (typeof sp == 'object') {
overrides = sp; //將第三個參數換爲對象
sp = sb; //把第一個參數賦值第二個當成父類
sb = overrides.constructor != oc ? overrides.constructor : function() {
sp.apply(this, arguments);
}; //子類這個構造函數要麼是外界傳入的名字爲constructor,要麼就是直接調用超類構造函數的一個函數
//傳入的constructor除了構造自己還要調用超類的構造函數
}
/**
* 繼承的兩種參數
* 1,自己寫一個構造函數,初始化一些字段,然後調用超類構造函數,再寫一個json對象,裏面是要覆蓋超類的方法或者追加的方法
* 然後這樣調用extend(sub,sup,{over1:f,over2:f,addf:f}),就像java的語法
* SubClass extend SuperClass {
* SubClass(){
* super();
* }
* }
*
* 2,第一種可以理解爲模擬java,但是因爲構造函數也是數據,所以完全可以把構造函數也放進那個jdon對象,只不過約定好一個名字
* 比如constructor,然後這樣調用
* extend(sup,{constructor:f,over1:f,over2:f,addf:f})
*/
var F = function() {
},
sbp,
spp = sp.prototype;
F.prototype = spp;
sbp = sb.prototype = new F();
//以上用乾淨函數嫁接得到子類原型
sbp.constructor = sb; //然後指定一個constructor指回子類,這樣就大工告成
sb.superclass = spp; //在子類上指定一個靜態字段指向超類原型,這樣在子類構造函數中可訪問超類構造函數sub.superclass.constructor.call(this, config)
/**
* 這段代碼是防禦性的,在自己實現繼承的時候,可能會出現原型上的構造函數指向問題,所以如果發現某個超類
* 的構造函數是object,要麼這個超類卻是Object,要麼出現了失誤,所以這裏再一次重設置一下,以防萬一,這個代碼我們在分析Ext的Observable的時候會提到的它的作用
*/
if (spp.constructor == oc) {
spp.constructor = sp;
}
//子類上方一個靜態的重寫方法,注意js沒有重載,可以用來重寫子類原型上的函數
sb.override = function(o) {
Ext.override(sb, o);
};
//用一個閉包在子類原型上引用一個超類原型,引用的是一個函數
sbp.superclass = sbp.supr = (function() {
return spp;
});
//子類原型上放置一個重寫函數,可以用來覆蓋具體實例對象
sbp.override = io;
//在子類原型上重寫或添加函數
Ext.override(sb, overrides);
//子類上直接放一個靜態繼承方法,貌似實現多繼承
sb.extend = function(o) {
return Ext.extend(sb, o);
};
return sb;
};
}();
現在使用Ext的extend來實現我們之前的繼承代碼就如下
this.x = o.x;
this.y = o.y;
};
Plane.prototype.XY = function() {
alert(this.x * this.y);
};
var Space = Ext.extend(Plane, {
constructor : function(o) {
Space.superclass.constructor.call(this, o);
this.z = o.z;
},
XYZ : function() {
alert(this.x * this.y * this.z);
}
});
var space = new Space({ x:2,y:3,z:4});
space.XY();
space.XYZ();
現在我們來分析一下Ext中的繼承重頭戲Observable,它位於Ext.util這個包下,它的意思即是觀察者,使用觀察者模式,EDA模式,UI組件就利用這種基於觀察和事件的機制進行通信和渲染。
var me = this, e = me.events;
if(me.listeners){
me.on(me.listeners);
delete me.listeners;
}
me.events = e || {};
};
constructor: function(config) {
this.name = config.name;
//把監聽器放進超類的屬性
this.listeners = config.listeners;
this.events = {"change" : true};
//給領域模型設置事件,通過上面的寫法也可以
/* this.addEvents({
"change" : true
});*/
//調用超類構造超類不變量
ForumThread.superclass.constructor.call(this, config)
},
//領域行爲,會觸發事件
changeName : function(newName) {
alert("原主題名字是:" + this.name);
this.name = newName;
alert("更改後主題名字是:" + this.name);
this.fireEvent("change", this);//觸發事件
}
});
Ext.onReady(function() {
var forumThread = new ForumThread({
name : '關於將Jdon框架提升爲DCI框架的設想',
//構造領域模型時注入監聽處理程序
listeners : {
change : function(thread) {
alert('接受到事件,將異步保存新的名字:' + thread.name);
}
}
});
//領域行爲調用
forumThread.changeName("關於將Jdon框架提升爲DCI框架的設想,整合JdonMVC");
});
如果事件設置和監聽綁定直接在子類完成,那麼就不必顯式調超類構造函數
constructor: function(config) {
this.name = config.name;
this.events = {"change" : true};
this.on(config.listeners);
},
//領域行爲,會觸發事件
changeName : function(newName) {
alert("原主題名字是:" + this.name);
this.name = newName;
alert("更改後主題名字是:" + this.name);
this.fireEvent("change", this);//觸發事件
}
});