在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的寫法,如下:
- var Plane = function(x, y) {
- 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 extend = function(sb, sp, sbm) {
- 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的代碼如下:
- extend(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 extend2 = function(sp, sbm) {
- 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就可以了。
這個時候我們新的繼承語法如下:
- var NewSpace = extend2(Plane, {
- 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的實現比較,大同小異
- // propertIEs are directly passed to `create` method
- var Person = Class.create({
- initialize: function(name) {
- this.name = name;
- },
- say: function(message) {
- return this.name + ': ' + message;
- }
- });
- var Animal = new Class({
- 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;
- }
- });
到了這裡其實已經差不多了,但是細心的讀者會發現,我們在extend中會把sbm的所有屬性拷貝到子類的原型上,這裡豈不是就要把constructor也拷貝到原型上?如果sbm包含了這個constructor其實就無所謂,因為子類的原型的constructor本來就是需要指向這個構造函數的,但是sbm上沒有constructor那豈不是要把Object拷貝到子類原型上,答案是不會的,我們在拷貝的時候用的for in循環是迭代不出默認的那個constructor的。
現在我們來看看Ext.extend,應該完全沒有問題了。我們用了兩個方法extend,extend2,Ext把它合並為了一個方法Ext.extend,所以它會判斷傳進來的參數然後進行變換,這樣Ext.extend就支持兩個參數和三個參數進行調用。對於前面用到拷貝屬性,Ext做了一個工具函數叫做Ext.apply,對於將一個對象的屬性拷貝到一個類的原型上,Ext做了一個工具類叫做Ext.override。
- Ext.extend = function() {
- // 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來實現我們之前的繼承代碼就如下
- var Plane = function(o) {
- 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組件就利用這種基於觀察和事件的機制進行通信和渲染。
所有的UI組件都繼承這個類,我們看看它的構造函數
- EXTUTIL.Observable = function(){
- var me = this, e = me.events;
- if(me.listeners){
- me.on(me.listeners);
- delete me.listeners;
- }
- me.events = e || {};
- };
這個構造函數不需要參數,在Java中,這種父類的構造可以自動的調用默認構造函數,但是這裡要注意,if(me.listeners)依賴了子類的構造行為,這在面向對象原則中似乎是一個禁忌,但是如果一個繼承體現完全由一個團隊維護,他們同時制定繼承規則和繼承規范,這也無可厚非,這裡的listeners可以在子類中不提供,可以讓構造出來的對象自己調用on方法來添加監聽器,同理這裡的events,如果子類沒構造會被賦值為一個空對象。那麼這個Observable構造器做了兩個事,一個是看子類是否在對象上放了監聽器,如果放了,就調用對象的on方法進行事件和監聽的綁定,二是看子類是否在對象上放置了events,如果沒有就把對象的events屬性設置為一個空對象。也就是說子類是完全可以不做任何事的,子類只負責自己的數據構造和行為覆蓋或追加,events在和監聽器綁定之後就是一個Ext.util.Event對象的容器,見這行代碼: me.events[eventName] = ce = new EXTUTIL.Event(me, eventName);也就是說,在Ext中,一個活生生的能夠響應事件的對象有一個Event容器,它保存了這個對象可以響應什麼事件以及事件被觸發後被調用的監聽器。
Observable原型上放置的方法都是子類繼承的方法,子類的對象就可以在運行時調用這些方法,如下:
原型上放置了一個靜態變量和一些方法,這些方法都是和事件以及監聽有關,注意Observable的原型是一個新的對象直接量,它的constructor屬性肯定指向的是Object,不是指向的Observable,這豈不是存在bug,我通過代碼檢測發現alert(Ext.util.Observable.prototype.constructor == Ext.util.Observable);的結果確實又是true,怎麼回事呢?答案就在Ext.extend的那段防御性代碼,大家回過去看看吧!
現在我們寫一個繼承Obervable的類,不過不是UI組件,而是一個領域模型,比如論壇帖子,它在被修改之後會跑出一個被修改的事件,監聽器捕獲這個事件將修改保存到數據庫中,代碼如下:
- ForumThread = Ext.extend(Ext.util.Observable, {
- 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");
- });
如果事件設置和監聽綁定直接在子類完成,那麼就不必顯式調超類構造函數
- ForumThread = Ext.extend(Ext.util.Observable, {
- 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);//觸發事件
- }
- });