2014年1月2日 星期四

ExtJS 4 測試 : 類別與物件

Javascript 是一個 classless (無類別) 的語言, 本質上屬於函數式語言 (functional), 雖然有內建物件, 也可以自訂物件, 但那是透過 prototype 來實作, 所以只能算是 prototype-based 的物件導向, 不是像 Java 那樣的純粹物件導向語言 (class-based). 

ExtJS 利用 Javascript 的 prorotype 物件模擬 (emulate) 出一個像 Java 那樣的跨平台物件導向用法的框架, 它是眾多 Javascript 框架中, 極少數為熟悉物件導向開發者而設計的 API, 其函式庫是模仿 Java 用套件 (package) 與類別 (class) 組織起來的, 對已熟悉物件導向的程式設計者而言, 以 ExtJS 做為前端網頁架構, 在學習上較容易上手.

ExtJS 在第四版重新設計了其類別系統, 結構上與 ExtJS 3 有所不同, 而且添加了新的功能. 但為了向後相容 (backwar compatible), 也利用別名 (alias 或 AlternateClassName) 來讓 ExtJS 3 程式能以最低痛苦指數移植到 Ext JS4. 其 API 文件與類別相關原始碼如下 :

http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.MessageBox
Ext.Base 原始碼
Ext.ClassManager 原始碼

類別與套件命名方式

ExtJS 4 的基礎類別為 Ext.Base, ExtJS 的所有類別均繼承自 Ext.Base.

ExtJS 的類別名稱以大寫字母開頭, 駝峰格式命名 (camel case), 例如 :

Ext.ComboBox
Ext.TabPanel
Ext.LocalStorage

Ext JS 內建了許多類別, 每一個都是以 "Ext." 開頭. Ext 是所有內建類別的根命名空間 (root name space), 它是一個 Javascript 全域物件 (global object), 用來裝載全部的 ExtJS 4 內建類別. 事實上, 在 API 的 src 目錄下的 Ext.js 原始碼第一行即定義 :

var Ext = Ext || {};

所以 Ext 一開始就是個空物件 {}, 然後才定義其各項屬性與方法.

ExtJS 4 所有內建類別均以 Java 的階層式套件方式管理, 實際上就是反映各類別的 js 原始檔在目錄結構中的存放位置, 例如負責向伺服器取得後台資料的 Ext.data.proxy.Ajax 類別, 其原始碼 Ajax.js 就放在 \src\data\proxy 下, 而 "Ext.data.proxy" 就是 Ajax 類別的命名空間, data 稱為一個套件 (package), proxy 是子套件 (sub-package).


大部分的 ExtJS 內建類別都分門別類放在套件中, 亦即放在較低階層的名稱空間裡, 但有一部份的屬性與公用函式是直接放在 Ext 這個根名稱空間下面, 例如判斷瀏覽器版本的  Ext.isIE9 屬性, 定義類別的 Ext.define() 方法與建立物件的 Ext.create() 方法等等.

如何定義類別與建立物件

撰寫 ExtJS 程式的過程其實就是創造自己的類別 (不繼承內建元件類別), 或者利用 ExtJS 的內建類別衍生出我們自己的類別 (繼承內建元件類別), 再以此自訂類別為模子, 壓製出所需之物件來建構應用程式. 所以, 物件導向的程式設計就像玩樂高堆積木一樣, ExtJS 已經幫我們準備好形形色色的積木, 我們要學習的, 就是如何從箱子裡找到合用的積木來實現想像力.

ExtJS 4 的新類別系統已經改用 Ext.define() 方法來定義類別, 取代 ExtJS 3 所使用的 Ext.extend(); 而建立物件則改用 Ext.create() 方法, 取代 ExtJS 3 所用的 new. 

Ext.define() 事實上是 Ext.ClassManager.create() 方法的別名, 而 Ext.create() 則是 Ext.ClassManage.instantiate() 方法的別名. 當我們用 Ext.define() 定義一個類別時, 在 ExtJS 內部事實上是呼叫  Ext.ClassManager.create() 方法來建立一個 Ext.Class 類別的實體, 其建立過程是將我們傳入之參數分成 pre-processing (前處理) 與 post-processing (後處理) 兩階段 :


我們可以呼叫 Ext.Class 類別的 getDefaultPreprocessors() 方法取得前置處理器名稱, 此方法會傳回一個陣列, 因此可以用 Ext.each() 來拜訪它 (關於迴圈的探討, 請參閱 : ExtJS 4 測試 : each 與 forEach 迴圈測試) :

前置處理http://mybidrobot.allalla.com/extjstest/preprocessor.htm [看原始碼]

      var pre="";
      Ext.each(Ext.Class.getDefaultPreprocessors(),
               function(item,index){
                 pre += "pre[" + index + "]=" + item + "<br>";
                 }
               );
      Ext.Msg.alert("Pre-processor",pre);


可見它會先解析類別名稱, 處理名稱空間之後再處理動態載入, 繼承, 靜態成員, 設定項, 混入類別, 以及類別別名. 而後置處理器名稱則可透過 Ext.ClassManager 類別的 defaultPostprocessors 屬性取得, 其值也是一個陣列 :

後置處理http://mybidrobot.allalla.com/extjstest/postprocessor.htm [看原始碼]

      var post="";
      Ext.each(Ext.ClassManager.defaultPostprocessors,
               function(item,index){
                 post += "post[" + index + "]=" + item +
"<br>";
                 }
               );
      Ext.Msg.alert("Post-processor",post);

 

新的類別系統有兩個好處 :
  1. ExtJS4 已經將宣告 Namespace 的功能整合到 Ext.define() 的第一參數中, ExtJS 4 會自動偵測並建立 Namespace, 因此不需要再像 ExtJS 3 那樣, 在定義類別之前先呼叫 Ext.namespace() 來宣告名稱空間. 同時, 若有繼承其他類別時, 類別管理器會自動檢查類別的相依關係, 不用再擔心類別的載入順序. 
  2. 使用 Ext.create() 會在物件實體化前, 自動載入與類別相依的全部 Javascript 檔案, 這種功能稱為動態載入 (dynamic loading). 雖然 ExtJS 3 使用的之 new 仍然可用, 但是這樣就無法使用 ExtJS 4 新類別系統的動態載入功能.
類別的定義語法格式為 :

Ext.define("MyApp.package.subPackage.MyClass",
                     {  //在此定義類別之屬性, 方法, 以及繼承關係  }
                      );

建立物件的語法格式為 :

var myObj=Ext.create("MyApp.package.subPackage.MyClass", [config]); 


Ext.define() 的第一個參數是個字串, 用來宣告類別的全名, 完整的名稱建議包括應用程式名稱 (MyApp), 套件名稱 (package), 子套件名稱 (subPackage), 以及類別名稱 (MyClass), 以點號串接. 其中前面的 MyApp.package.subPackage 稱為類別 MyClass 的名稱空間 (name space), 用來管理類別結構, 區隔其他同名類別避免衝突. 多層次的名稱空間可以降低衝突的機率.

ExtJS 4 採用字串 (string-based) 為基礎的方法來定義名稱空間與類別, ExtJS 框架會自動將此字串解析出名稱空間與類別, 再進行類別之建立程序. 命名習慣是除類別用首字母大寫的駝峰字外, 其他均用首字母小寫的駝峰字 (如套件名稱, 屬性與方法名稱), 名稱應僅由字母與數字組成 (但不可用數字開頭), 不要使用符號.

注意, 使用 Ext.define() 定義類別時, 名稱空間不是必要的, 亦即 Ext.define("MyClass") 也是合法的, 但當類別增多時可能會有同名衝突問題.

Ext.define() 的第二個參數是類別內容, 以 JSON 物件實體來宣告類別的屬性 (property) 與方法 (method), 方法是用函式來定義. 定義類別可以想成是打造一個紅龜粿的模子, 而建立物件就是用這模子來製作紅龜粿.

Ext.create() 方法的第一個參數是傳入類別名稱, 第二個參數是用來設定屬性的物件, 會傳給建構子 (constructor) 設定各屬性之值, 但它是備選 (optional) 參數. config 物件格式如下 :

var config={property1:value1, property2:value2, ....}

呼叫 Ext.create() 方法時, ExtJS 會先判斷傳入的第一參數是不是 ExtJS 的內建類別 (搜尋內建類別表), 如果沒找到, 那就是自訂類別, 這時就要拆解名稱空間, 然後去 Ext.global 屬性中去找之前是否有用 Ext.define() 定義過此類別.

下列範例 1 展示如何自訂一個類別, 並依此建立其物件 (又稱為實例, instance) :

測試範例 1 : http://mybidrobot.allalla.com/extjstest/class_1.htm [看原始碼] 

      Ext.define("MyApp.Users",{
        account : "Tony",
        password : "12345",
        email : "tony@abc.com",
        getInfo : function(){
           return "account=" + this.account + " " +
                      "password=" + this.password + " " +
                      "email=" + this.email;
           }
        });
      var user1=Ext.create("MyApp.Users");
      Ext.Msg.alert("訊息",
                              "Hello! " + user1.account,
                              function(){Ext.Msg.alert("訊息",user1.getInfo());}
                              );



這裡我們宣告了三個屬性 (又稱為欄位, field) : account, password, email, 與一個方法 getInfo(). 當我們建立此類別的物件 user1 後, 就可以用點符號存取其屬性與方法. 在上例中, 我們用 user1.account 取得其帳號屬性值, 用 user1.getInfo() 呼叫其方法, 然後以兩層 ExtJS 訊息框顯示結果, 關於訊息框用法, 請參考 :

# ExtJS 4 測試 : 對話框與進度條

類別別名 alias

我們也可以用 alias 屬性為類別取一個簡短的別名, 這樣在建立物件時就可以不需敲這麼長的全名, 如下範例 1-1 所示 :

測試範例 1-1http://mybidrobot.allalla.com/extjstest/class_1_1.htm [看原始碼]

      Ext.define("MyApp.Users",{
        alias:"Users",
        account:"Tony",
        password:"12345",
        email:"tony@abc.com",
        getInfo:function(){
          return "account=" + this.account + " " +
                 "password=" + this.password + " " +
                 "email=" + this.email;
          }
        });
      var user1=Ext.create("Users");
      Ext.Msg.alert("訊息","Hello! " + user1.account,
                    function(){
                      Ext.Msg.alert("訊息",user1.getInfo());
                      }
                    );

上例顯示經 alias 指定 MyApp.Users 之別名為 Users 後, 就可以用 Users 來建立物件了.


建構子 (Constructor)

建構子是建立物件時第一個被呼叫的方法, 用來設定物件屬性的初始值 (所以叫做建構子). 在上面範例 1 中, 我們在定義類別時就預設其屬性值為特定值了, 雖然這沒啥不對, 這對於類別是一個一般化的模子這概念而言, 似乎不夠一般啊! 帳號為什麼不用 Peter, 偏要用 Tony?, 用空字串不是更一般嗎?

比較自然的方式是將預設值設為空字串, 再用建構子方法來設定屬性值. 在 ExtJS 4 中, 建構子方法是用特定屬性名稱 constructor 設定, 此方法傳入一個屬性設定物件 config, 也就是用Ext.create() 方法建立物件時所傳入的第二個參數, 再用 this (表示物件本身) 來設定屬性值, 其語法如下 :

constructor : function(config) {
   this.property1=config.property1,
   this.property2=config.property2,
   .....
   }

我們將範例 1 改為範例 2 如下 :

測試範例 2http://mybidrobot.allalla.com/extjstest/class_2.htm [看原始碼] 

      Ext.define("MyApp.Users",{
        account : "",
        password : "",
        emai l: "",

        getInfo : function(){
          return "account=" + this.account + " " +
                     "password=" + this.password + " " +
                     "email=" + this.email;
          },
        constructor : function(config){
          this.account=config.account;
          this.password=config.password;
          this.email=config.email;
          }

        });
      var user1=Ext.create("MyApp.Users",
                                       {account:"Tony",
                                         password:"12345",
                                         email:"tony@abc.com"
                                         }

                                      );
      Ext.Msg.alert("訊息","Hello! " + user1.account,
                             function(){
                                Ext.Msg.alert("訊息",user1.getInfo());
                                }
                             );

結果與範例 1 一樣, 但使用 constructor 來初始化物件比較有 OO 感, 這樣在建立物件時, 傳入不同的設定物件, 就會得到不同屬性的物件, 比較符合類別是一個模子的觀念. 注意, constructor 方法中的參數 config 就是 Ext.create() 方法所傳入的第二個參數.

其實, 初始化參數也可以不用 config 物件形式, 而是直接依序傳進去, 如下列範例 2-1 所示 :

測試範例 2-1http://mybidrobot.allalla.com/extjstest/class_2_1.htm [看原始碼]

      Ext.define("MyApp.Users",{
        account:"",
        password:"",
        email:"",
        getInfo:function(){
          return "account=" + this.account + " " +
                 "password=" + this.password + " " +
                 "email=" + this.email;
          },
        constructor:function(account,password,email){
          this.account=account;
          this.password=password;
          this.email=email;
          }
        });
      var user1=Ext.create("MyApp.Users","Tony","12345","tony@abc.com");
      Ext.Msg.alert("訊息","Hello! " + user1.account,
                    function(){
                      Ext.Msg.alert("訊息",user1.getInfo());
                      }
                    );

但不建議用此方式, 因為 create() 方法中的引數必須與 constructor 中的參數一一對應, 當屬性多時就容易混淆, 還是用上面範例 2 的 config 物件較明確.

設值器與取值器 (setter & getter)

在上面的例子中, 我們用 user1.account 來取得物件的屬性值, 在物件導向設計中, 應該透過 API (也就是 setter 與 getter 方法) 來存取屬性, 而非利用點號直接存取屬性. 設值器與取值器功能很簡單, 前者是用 this.property=value 來設定屬性值; 而後者則是用 return this.property 傳回屬性值. 如下範例 2-2 所示 :

測試範例 2-2http://mybidrobot.allalla.com/extjstest/class_2_2.htm [看原始碼]

      Ext.define("MyApp.Users",{
        account:"",
        password:"",
        email:"",
        getInfo:function(){
          return "account=" + this.account + " " +
                 "password=" + this.password + " " +
                 "email=" + this.email;
          },
        getAccount:function(){
          return this.account;
          },
        constructor:function(config){
          this.account=config.account;
          this.password=config.password;
          this.email=config.email;
          }
        });
      var user1=Ext.create("MyApp.Users",
                           {account:"Tony",
                            password:"12345",
                            email:"tony@abc.com"
                            }
                           );
      Ext.Msg.alert("訊息","Hello! " + user1.getAccount(),
                    function(){
                      Ext.Msg.alert("訊息",user1.getInfo());
                      }
                    );


但是建構子方法裡面一大堆 this 實在很囉唆, 因此 ExtJS 4 引進了一個 initConfig() 方法, 讓我們只要在 constructor 方法中呼叫此方法, 就會自動幫我們初始化物件之屬性. 但是有一個條件, 所有的類別屬性都要放到名為 config 的屬性中, 這樣 initConfig() 方法才能找到要設定的對象, 如下列範例 3 所示 :

測試範例 3 : http://mybidrobot.allalla.com/extjstest/class_3.htm [看原始碼]

      Ext.define("MyApp.Users",{
        config:{
          account:"",
          password:"",
          email:""
          },
        getInfo:function(){
          return "account=" + this.account + " " +
                    "password=" + this.password + " " +
                    "email=" + this.email;
          },
        constructor:function(config){
          this.initConfig(config);
          }
        });
      var user1=Ext.create("MyApp.Users",
                           {account:"Tony",
                            password:"12345",
                            email:"tony@abc.com"
                            }
                           );
      Ext.Msg.alert("訊息","Hello! " + user1.account,
                    function(){
                      Ext.Msg.alert("訊息",user1.getInfo());
                      }
                    );

可見用 config 與 initConfig() 後, 程式碼就簡潔多了, 也較省工, ExtJS 4 已經背後幫我們搞定初始化工作了. 但是切記類別的全部屬性都要放在 config 屬性中宣告, 否則 initConfig() 的初始化會破功 (雖然程式沒有語法錯誤), 如下列範例 3-1 所示 :  

測試範例 3-1 : http://mybidrobot.allalla.com/extjstest/class_3_1.htm [看原始碼] (無效)

除了呼叫 initConfig() 方法外, 也可以在建構子內呼叫 Ext.apply(this, config) 方法來初始化物件, 此 apply方法會將傳進來的設定物件 config 複製到物件本身 this, 也能達成初始化效果, 如下列範例 3_2 所示 :


測試範例 3-2http://mybidrobot.allalla.com/extjstest/class_3_2.htm [看原始碼]

        constructor:function(config){
          Ext.apply(this,config);
          }


使用 config 設定物件來初始化的好處不只是這樣, ExtJS 還會自動替每一個放在 config 裡面的屬性產生 setter 與 getter 方法, 其名稱就是 setProperty(value) 與 getProperty(), 如下面範例 4 所示 :


測試範例 4http://mybidrobot.allalla.com/extjstest/class_4.htm [看原始碼]

      Ext.define("MyApp.Users",{
        config:{
          account:"",
          password:"",
          email:""
          },
        constructor:function(config){
          this.initConfig(config);
          }
        });
      var user1=Ext.create("MyApp.Users",
                           {account:"Tony",
                            password:"12345",
                            email:"tony@abc.com"
                            }
                           );
      var info1="account=" + user1.getAccount() + " " +
                      "password=" + user1.getPassword() + " " +
                      "email=" + user1.getEmail();
      user1.setAccount("Peter");
      user1.setPassword("54321");
      user1.setEmail("peter@xyz.com");

      var info2="account=" + user1.getAccount() + " " +
                      "password=" + user1.getPassword() + " " +
                      "email=" + user1.getEmail();
      Ext.Msg.alert("info1",info1,
                    function() {
                      Ext.Msg.alert("info2",info2);
                      }
                    );



上例中先以 getter 取得物件建立時初始化所設定之使用者資訊, 接著用 setter 去修改物件屬性. 我們並沒有特地寫 setter 與 getter 方法, 但卻可以呼叫每一個屬性之設值與取值方法, 這都是在建構子方法中呼叫 initConfig() 或 Ext.apply() 時自動幫我們加上去的.  

接著我們要測試靜態成員 (Static Members),包含靜態屬性與靜態方法. 靜態成員是屬於類別的, 直接使用類別名稱存取, 不需要建立物件實體. 所有此類別所建立的全部物件實體都共用靜態成員, 因此一個物件更改了靜態成員之值, 其他物件也受到影響, 因為它們共用一個ExtJS 4 的靜態成員使用 statics 屬性宣告, 格式如下 :

statics : {
  staticProperty1 : value1, 
  staticProperty2 : value2,
  ...
  staticMethod1 : function() {...}, 
  staticMethod2 : function() {...},
  ...
  }

我們將範例 4 改為如下範例 4-1, 加入一個設定總使用者數目的靜態屬性 :

測試範例 4-1http://mybidrobot.allalla.com/extjstest/class_4_1.htm [看原始碼]

      Ext.define("MyApp.Users",{
        config:{
          account:"",
          password:"",
          email:""
          },
        statics:{
          totalUsers:0,
          resetTotalUsers:function(){
            this.totalUsers=0;
            }
          },
        constructor:function(config){
          this.initConfig(config);
          this.self.totalUsers++;

          //this.statics().totalUsers++; //用 this.statics() 亦可
          }
        });
      var user1=Ext.create("MyApp.Users",
                           {account:"Tony",
                            password:"12345",
                            email:"tony@abc.com"
                            }
                           );
      var user2=Ext.create("MyApp.Users",
                           {account:"Peter",
                            password:"54321",
                            email:"peter@abc.com"
                            }
                           );
      var info="totalUsers=" + MyApp.Users.totalUsers;
      Ext.Msg.alert("info",info,callBack);
      function callBack(info){
        MyApp.Users.resetTotalUsers();
        var info="totalUsers=" + MyApp.Users.totalUsers;
        Ext.Msg.alert("info",info);
        }




上例中, 我們用 statics 宣告了一個靜態屬性 totalUsers, 用來記錄總用戶人數, 也定義了一個靜態方法 resetTotalUsers() 用來將 totalUsers 歸零. 在建構子方法中, 可用 this.self 來取得類別本身的參考, this.self.totalUsers 即取得此靜態變數, 然後將其增量. 因此每次呼叫 Ext.create() 建立一個物件實體時, 靜態變數 totalUsers 就會加 1. 我們使用類別名稱 MyApp.Users 直接存取靜態變數與方法, 並於 Ext.Msg.alert() 的回呼函式中, 呼叫靜態方法 resetTotalUsers() 將 totalUsers 歸零. 關於對話框請參閱 "ExtJS 4 測試 : 對話框與進度條".

這裡值得一提的是 this 與 self 這兩個會令人感到疑惑的關鍵字. 在 Javascript 中, this 表示目前的物件, 上例中 statics 屬性之值為一個物件實例, 所以在其內部方法 resetTotalUsers() 裡要存取自己物件的 totalUsers 屬性時就要用 this, 這個 this 指的就是 statics 這個物件. ExtJS 4 的 this 就是 Javascript 的 this, 代表目前物件的參考, 指向一個物件實例. 但是在建構子方法 constructor 中, this 指的卻是一個 MyApp.Users 物件, 因為建構子不是 statics 物件的成員, 要存取 statics 物件之成員必須透過類別本身, 所以 ExtJS 4 創造了一個 self 來指涉一個物件所屬類別的參考 (跟 Java 的 getClass 方法類似), this.self 就是指向此物件之類別本身  (事實上就是一個 Ext.Class 物件). 每次用 Ext.create() 建立物件實例時, ExtJS 都會在每一個實例中建立一個指向其類別之 self 參考. 注意, this 與 self 都是在類別內部使用, 在類別外部要用類別名稱來存取靜態成員. 

上面提到靜態成員屬於類別, 那靜態成員可不可以用物件實體來存取呢? 答案是不行的, 讀取屬性會得到 undefined, 用物件呼叫方法則會報出 "Uncaught Type Error (無此方法)",  如下列範例 4-2 所示 :

測試範例 4-2http://mybidrobot.allalla.com/extjstest/class_4_2.htm [看原始碼]

      Ext.define("MyApp.Users",{
        config:{
          account:"",
          password:"",
          email:""
          },
        statics:{
          totalUsers:0,
          resetTotalUsers:function(){
            this.totalUsers=0;
            }
          },
        constructor:function(config){
          this.initConfig(config);
          this.statics().totalUsers++;
          }
        });
      var user1=Ext.create("MyApp.Users",
                           {account:"Tony",
                            password:"12345",
                            email:"tony@abc.com"
                            }
                           );
      var user2=Ext.create("MyApp.Users",
                           {account:"Peter",
                            password:"54321",
                            email:"peter@abc.com"
                            }
                           );
      var info="user1.totalUsers=" + user1.totalUsers + "<br>" +
                    "user2.totalUsers=" + user2.totalUsers;
      Ext.Msg.alert("info",info);





可見, 靜態成員是屬於類別的, 不屬於物件.

接著要測試物件導向最重要的特性 : 繼承, 與此相關的設定屬性為 extend 與 mixins.

繼承 (inheritance) : extend 與 mixins

物件導向訴求元件的重用性 (reusable), 亦即 "不重新打造輪子", 最重要的特性便是繼承機制. C++ 允許多重繼承, Java 為了避免混淆與複雜而採用單一繼承, ExtJS 4 也是單一繼承, 其 extend 屬性僅能指定一個類別名稱 (字串而非陣列), 但透過 mixins 屬性卻擁有多重繼承的效果 , 可指定要從哪些類別 (字串陣列或物件) 複製其屬性與方法.

首先, 我們來測試 extend 屬性的用法, 此屬性值為所要繼承之類別名稱 (字串), 如下面範例 5 所示 :

測試範例 5http://mybidrobot.allalla.com/extjstest/class_5.htm [看原始碼]

      Ext.define("Myapp.Vehicle",{    //父類別
        config:{
          onwhere:"land",  //land,air,water,space
          power:"oil"      //oil,electric,hybrid,wind,human,animal
          },
        constructor:function(config){
          this.initConfig(config);
          },
        move:function(dir){return "Vehicle is moving " + dir + "..."},
        turn:function(dir){return "Vehicle is turning " + dir + "..."},
        stop:function(){return "Vehicle stops."}
        });
      Ext.define("Myapp.Car",{  //子類別
        extend:"Myapp.Vehicle",  //繼承
        config:{
          type:"van", //sports,van,truck
          wheels:4
          },
        constructor:function(config){
          this.initConfig(config);
          },
        move:function(dir){return "Car is moving " + dir + "..."},
        turn:function(dir){return "Car is turning " + dir + "..."}
        });
      var myCar=Ext.create("Myapp.Car",
                           {type:"sports",
                            wheel:4,
                            power:"hybrid"
                            }
                           );
      var info="type=" + myCar.getType() + "<br>" +
               "wheel=" + myCar.getWheels() + "<br>" +
               "power=" + myCar.getPower() + "<br>" +
               myCar.move("forword") + "<br>" +
               myCar.turn("right") + "<br>" +
               myCar.stop();
      Ext.Msg.alert("info",info);


在上例中, 我們定義了 Vehicle 與 Car 兩個類別, 其中 Car 有一個 extend 屬性指向 Vehicle, 表示繼承 Vehicle 的全部屬性與方法, 故 Car 是 Vehicle 的子類別, 當然也擁有了 move, turn, 與 stop 三個方法, 但是, 我們在 Car 類別中又重複定義了 move 與 turn 兩個方法, 這稱為方法覆蓋 (method override), 即新定義的方法將覆蓋繼承而來的同名方法. 當父類別的方法不合用時, 我們就可以透過 override 製作自己的方法. 因此上圖中呼叫此三方法時, 僅 stop 方法為執行父類別的 stop 方法, 顯示 "Vehicle stops".

緊接著我們要測試 mixins (混入), 這是物件導向中的一個觀念. 關於 Mixin 的說明, 可參考下列文章 :

  1. 維基百科 : Mixin 多重繼承
  2. 用 MIXIN 設計模式來作多重繼承
  3. 談多重繼承:Ruby 利用 Mix-in 實現多重繼承

要言之, 因為多重繼承 (例如 C++ 與 Python) 有容易造成混淆的缺點 (例如名稱的衝突), 比較複雜, 因此 Java 在實現繼承 (implement) 上採用單一繼承, 後來的 C#, Ruby 也都是單一繼承. 但是 Java 允許類別在抽象層面上可以實作多個介面 (interface), 利用此方式也可以達成多重繼承的功能. 而 Ruby 則用 mixin 來實現多重繼承, ExtJS 也是如此.

我們將範例 5 稍微改一下來測試 mixins 的功能, 如下範例 6 所示 :

測試範例 6http://mybidrobot.allalla.com/extjstest/class_6.htm [看原始碼]

      Ext.define("Myapp.Activity",{
        src:"mixin",
        move:function(dir){return "Act: moving " + dir + "..."},
        stop:function(){return "Act: stops"}
        });
      Ext.define("Myapp.Vehicle",{
        config:{
          onwhere:"land",  //land,air,water,space
          power:"oil"      //oil,electric,hybrid,wind,human,animal
          },
        constructor:function(config){
          this.initConfig(config);
          },
        move:function(dir){return "Vehicle is moving " + dir + "..."},
        });
      Ext.define("Myapp.Car",{
        extend:"Myapp.Vehicle",
        mixins:["Myapp.Activity"],
        config:{
          src:"Myapp.Car",
          type:"van", //sports,van,truck
          wheels:4
          },
        constructor:function(config){
          this.initConfig(config);
          },
        turn:function(dir){return "Car is turning " + dir + "..."}
        });
      var myCar=Ext.create("Myapp.Car",
                           {type:"sports",
                            wheel:4,
                            power:"hybrid"
                            }
                           );
      var info="type=" + myCar.getType() + "&ltbr>" +
               "wheel=" + myCar.getWheels() + "&ltbr>" +
               "power=" + myCar.getPower() + "&ltbr>" +
               myCar.move("forword") + "&ltbr>" +
               myCar.turn("right") + "&ltbr>" +
               myCar.stop() + "<br>" +
               myCar.mixins.src + "<br>" +
      Ext.Msg.alert("info",info);


在上例中, 我們新定義了一個類別 Activity, 含有一個屬性 src 與兩個方法 : move() 與 stop(). 在定義 Car 類別時, 我們用 mixins 屬性指定混入之類別名稱 MyApp.Activity. 因為 ExtJS 採單一繼承, 所以 extend 屬性值為一個字串 (而非陣列或物件), 故只能指定一個父類別; 但是 mixins 屬性值則為一個陣列 (或物件), 可以指定要混入之多個類別.

要注意的是, mixins 雖然能夠達到類似多重繼承的功效, 但是它跟繼承還是不完全一樣 : 混入的類別屬性與方法不會覆蓋原有的成員, 例如上面的 Car 類別從 Vehicle 繼承了 move() 方法, 也從 Activity 類別混入 move() 方法, 但是呼叫 move() 時卻是呼叫從 Vehicle 繼承來的方法. 在屬性方面]也是一樣, Car 與 Activity 都有 src 屬性, 但是讀取 myCar.src 時卻讀到 Car 的 src.

總之, mixins 最主要的功能就是能使用其他類別的屬性與方法, 達到程式碼重用之目的, 彌補 extend 只能繼承單一類別的缺點. 但是 mixins 的類別沒有 override 功能, 因此存取同名的成員時不會存取到對混入的成員, 如果要存取混入類別的成員該怎麼做呢? 這時 mixins 屬性必須用物件, 這樣混入之類別就會有一個 key, 透過此 key 便可存取混入之類別成員了. 我們將範例 6 稍作修改為下列範例 7 :

測試範例 7http://mybidrobot.allalla.com/extjstest/class_7.htm [看原始碼]

      Ext.define("Myapp.Activity1",{
        move:function(dir){return "Act1: moving " + dir + "..."}
        });
      Ext.define("Myapp.Activity2",{
        stop:function(){return "Act2: stops"}
        });
      Ext.define("Myapp.Vehicle",{
        config:{
          onwhere:"land",  //land,air,water,space
          power:"oil"      //oil,electric,hybrid,wind,human,animal
          },
        constructor:function(config){
          this.initConfig(config);
          }
        });
      Ext.define("Myapp.Car",{
        extend:"Myapp.Vehicle",
        mixins:{act1:"Myapp.Activity1",act2:"Myapp.Activity2"},
        config:{
          src:"Myapp.Car",
          type:"van", //sports,van,truck
          wheels:4
          },
        constructor:function(config){
          this.initConfig(config);
          },
        move:function(dir){return "Car is moving " + dir + "..."},
        turn:function(dir){return "Car is turning " + dir + "..."}
        });
      var myCar=Ext.create("Myapp.Car",
                           {type:"sports",
                            wheel:4,
                            power:"hybrid"
                            }
                           );
      var info="type=" + myCar.getType() + "<br>" +
               "wheel=" + myCar.getWheels() + "<br>" +
               "power=" + myCar.getPower() + "<br>" +
               myCar.move("forword") + "<br>" +
               myCar.turn("right") + "<br>" +
               myCar.stop() + "<br>" +
               myCar.mixins.act1.move("backward") + "<br>";
      Ext.Msg.alert("info",info);


上例中, 我們定義了兩個類別 Activity1 與 Activity2, 並混入 Car 類別中. 由於 Car 自己也定義了一個 move() 方法, 如上所述, 混入類別不會覆蓋已有之方法, 因此 myCar.move() 會呼叫 Car 類別自己的 move() 方法. 如果要呼叫混入類別之同名方法, 就必須用物件的 mixins 屬性與混入類別的 key 取得混入類別之實體, 再呼叫其方法, 例如 myCar.mixins.act1.move(), 這  act1 就是用物件宣告 mixins 類別時, 指定給該混入類別之 key. 事實上, 如果沒有這樣的用途時, mixins 的屬性值用陣列即可, 例如 :

mixins : ["class1", "class2", ... ]

OK, 現在回過頭來看看在繼承父類別時, 靜態成員會有何影響.

~~~未完待續~~~

6 則留言:

  1. 小狐狸大您好,小弟是任職某公司的小小前端,公司最近要我去學ExtJS,剛好看到你撰寫的文章,雖然公司用的是5.0的版本,但整個程式架構及脈絡在您文章的幫助下清楚了不少,很感謝您用心寫的筆記,讓剛學習的人不致於瞎子模象,您的部落格主題內容很豐富,幾乎包山包海了,您的人生經歷我相信一定很精采,祝您未來順心,事事順利

    回覆刪除
  2. 作者已經移除這則留言。

    回覆刪除
  3. Hi, 感謝您留言, 讓我今日心情大好. 雖然撰寫筆記只是作為自己的備忘錄 (我現在已忘光 ExtJS, 但並非進入無招勝有招境界, 真的是純忘記), 但能聽到它為您帶來些許幫助, 還是感到很高興. ExtJS 我也沒學完, 要學的東西實在太多, 不知何時會回來.

    回覆刪除
  4. 另, 文中測試連結已搬家, 但沒空更新, 忙完這陣子優先更新. 您可先自行改為 :

    http://tony1966.xyz/test/extjstest/測試檔名.htm

    回覆刪除
  5. 例如 :

    http://tony1966.xyz/test/extjstest/preprocessor.htm

    回覆刪除
  6. 小狐狸大您好,謝謝提醒新的測試連結
    ExtJS真的很大包,不是這麼好入門,再加上它也不是目前業界主流的前端框架,網路上的資源並沒很多,很多情況下只能應硬吞它的官方API文件。
    總之您整理的文章真的很棒,雖然它是您個人的隨手筆記,但一點也不馬虎,希望小弟未來有若有問題,還可以請您不吝指教

    回覆刪除