Ext JS 4 的類系統

Liam Wang發表於2013-07-10

前言

我們知道,JavaScript中沒有真正的類,它是一種面向原型的語言 。這種語言一個強大的特性就是靈活,實現一個功能可以有很多不同的方式,用不同的編碼風格和技巧。但隨之也帶來了程式碼的不可預測和難以維護等問題。程式碼量很大時,由於JavaScript 沒有統一的結構,程式碼變得很難理解和閱讀,不方便維護和重用。

而像C#這種基於類的面嚮物件語言,它是強型別的,具有封裝、繼承和多型的OOP基本特徵,而且都有標準的編碼約定。它通過強制開發者遵循一系列的原則,讓編寫的程式碼更具有可預測性和可擴充套件性等優點。然而,這種語言卻不能像JavaScript語言一樣可以靈活使用。
這兩種語言各自都有他們的缺點和優點,能不能集合兩者的優點而摒棄它們的缺點呢?在 Ext JS 4 身上似乎能看到這種結合體的影子,讓我們來一看究竟。

命名規則

Ext JS 中名稱空間、包和類的層次關係如下圖所示:

基於類、名稱空間和檔名的一致的命名規範則,可以幫助我們良好地組織程式碼,使其有清晰的層次結構,並且容易閱讀。Ext JS 框架中的所有類都基於這種命名規則,同時官方建議使用者自定義的類也遵照這樣的規則。這個規則如何約定,具體如下:

1)類的命名約定

類名只可含字母和數字,數字雖然允許,但不推薦類的名字中含有數字,除非類名本身是一個專業術語。不要使用下劃線(_)、連字元(-)等其他非字母數字的字元。如:
MyCompany.util.Toolbar1 類名含數字,不推薦;
MyCompany.useful_util.Debug_Toolbar 含下劃線,不推薦;
MyCompany.util.Base64 Base64 合理。

類應該用包進行分組,至少有一個父層,且頂層一定是名稱空間,如:
MyCompany.util.Toolbar 合理;
MyCompany.Application 合理。

名稱空間和類的命名遵循CamelCased風格,包括簡寫詞,其它都應小寫,如:
MyCompany.form.action.AutoLoad 合理;
Ext.data.JsonProxy 合理;
Ext.data.JSONProxy 不推薦。

另外,為避免和Ext JS框架內部類發生衝突,使用者自定義類的頂層最好不要用Ext。

2)程式碼檔案

程式碼檔案的存放目錄應該和類的全名一一對應,最好是一個類存放一個檔案,如:
Ext.util.Observable 應存放在:專案原始碼目錄/Ext/util/Observable.js ;
Ext.form.action.Submit 應存放在:專案原始碼目錄/Ext/form/action/Submit.js;
MyCompany.chart.axis.Numeric 應存放在:專案原始碼目錄/MyCompany/chart/axis/Numeric.js。

3)方法、變數和屬性的命名約定

和類的命名規則類似,方法、變數和屬性的命名只可包含字母和數字,數字是允許的,但不推薦,除非專業術語。
方法和變數都應遵循camelCased風格,包括簡寫。
除了當屬性是常量時需全部大寫,其他情況完全和方法、變數的命名相同。
如:
合理的方法命名:encodeUsingMd5(), getHtml(), getJsonResponse(), parseXmlContent() ;
合理的變數命名:var isGoodName, var base64Encoder, var xmlReader, var httpServer 。
合理的屬性命名:Ext.MessageBox.YES = "Yes", MyCompany.alien.Math.PI = "4.13", MyCompany.Person.Name="Jim" 。

類的宣告/建立

在以前的 Ext JS 版本中用 Ext.extend 來建立一個類,在我另一篇博文ExtJS框架基礎:事件模型及其常用功能中有Ext.extend的例子。在以前的版本中為了遵循命名規範,我們可能這樣來建立一個類:

Ext.ns('My.cool');//Ext.ns是Ext.namespace 的簡寫.
My.cool.Window = Ext.extend(Ext.Window, { ... });

老方法中,My.cool.Window必需依賴於另一個已經存在的類建立。但在Ext JS 4中,建立一個類,只需一個方法,且可以不依賴另一個類建立,語法如下:

Ext.define(className, members, onClassCreated);

其中:className是類的全名;members是大括號括起來的一些鍵值對;onClassCreated是一個可選回撥函式,當該類建立完畢時呼叫。來看個實際的例子:

Ext.onReady(function () {
    Ext.define('My.sample.Person', {
        name: 'Unknown',
        constructor: function (name) {
            if (name) {
                this.name = name;
            }
        },
        eat: function (foodType) {
            alert(this.name + " is eating: " + foodType);
        }
    }, function () {
        alert("Person's instance has created!");
    });

    var jim = Ext.create('My.sample.Person', 'Jim');//alert("Person's instance has created!")
    jim.eat("Salad"); // alert("Jim is eating: Salad")
});            

 我們一般用Ext.create()來建立一個類的例項,也可以通過new關鍵字來建立,但推薦使用Ext.create(),Ext.create()能根據依賴關係動態載入建立新類所需要的類。

Ext JS 4中的OOP特性

我們知道,繼承、封裝和多型是OOP的三個基本特性。為讓大家能用大家熟悉的OOP思想程式設計,Ext JS 4也很好的實現了繼承、封裝和多型。
我們先來看看Ext JS 4 中的繼承,在上面的建立My.sample.Person類的例子中,我們再新建一個 My.sample.Student 類,並讓它繼承 My.sample.Person 類,如下:

Ext.define('My.sample.Student', {
    //繼承父類:My.sample.Person
    extend: 'My.sample.Person',
    //子類的構造器,如果子類有config配置項,則這項必需有。
    constructor: function (config) {
        this.initConfig(config);
        this.callParent([config]);
    },
    major: function (speciality) {
        //子類繼承了父類的所有屬性,比如下面的name。
        alert(this.name + " majored in: " + speciality);
        return this;
    }
});

//建立子類例項,父類的構造器先於子類構造器被呼叫。
var lily = Ext.create('My.sample.Student', 'Lily');
//呼叫父類中的eat方法
lily.eat('rice'); //alert("Lily is eating: rice")
//呼叫自己的major方法
lily.major('Computer'); //alert("Lily majored in: Computer")

要繼承一個父類,如果子類中有config配置項,則子類的constructor是必需的,不能省略。除了語法上的差別,Ext JS 4 中的繼承非常類似於C#中的繼承。

Ext JS 4 既然有繼承,那麼就肯定有OOP的另一個特性:多型。最明顯的是方法的覆蓋。在上面的Student例子中,如果要覆蓋父類的eat方法,只需加上自己的eat配置項:

...
eat: function (foodType, tool) {
    alert(this.name + " is eating " + foodType + " by " + tool+".");
},
...
//呼叫子類的eat方法
lily.eat('rice', 'tachyon');

在後面講的Mixins部分,其實也是一種多型。

封裝的特性就沒什麼好說的了,比如,Ext使用Ext.lib.Event、Ext.EventManager和Ext.EventObject對原生瀏覽器事件進行了封裝,最後給我們用的是一套統一的跨瀏覽器的通用事件介面。使用的時候我們無需關心其內部的實現。

相信對於學過OOP語言的朋友來說,這些特性應該非常好理解,大家在使用Ext JS 4 程式設計的時候再好好體會吧。下面讓我們繼續深入對 Ext JS 4 類的學習。

讀寫器的實現 - Config

Ext JS 4中的讀寫器意義上類似於C#屬性的讀寫器,只是語法上大不相同。Ext JS 4可通過config配置項實現對私有成員的封裝和對成員的讀寫控制。看下面的例子:

Ext.onReady(function () {
    Ext.define('My.own.Window', {
        isWindow: true,//只讀屬性。
        config: {
            //config中的配置項都可通過 getXxx()來讀取值。
        title: 'Title Here'
        },
        //構造器,初始化config
      constructor: function (config) {
            this.initConfig(config);
        },
        //applyTitle,Title對應config中的配置項title,寫值時內部自動呼叫。
        applyTitle: function (title) {
            //這裡可對寫值做一些控制
            if (!Ext.isString(title) || title.length === 0) {
                alert('Error: Title must be a valid non-empty string');
            }
            else {
                //在這裡可以對寫入的值進行加工。
                return title;
            }
        }
    });

    var myWindow = Ext.create('My.own.Window', {
        title: 'Hello World'
    });

    alert(myWindow.getTitle()); // alerts "Hello World"
    myWindow.setTitle('Something New');
    alert(myWindow.getTitle()); // alerts "Something New"
    myWindow.setTitle(null); // alerts "Error: Title must be a valid non-empty string"
});

注意,讀、寫器內部是自動執行的,所以要實現自動讀寫器一定要注意命名。如,config中的title配置項,它的寫值屬性一定是applyTitle,讀值和取值方法分別是getTitle()和setTitle()。

靜態成員 - Statics

我們知道,在C#中,我們可以直接通過類來訪問類中的靜態成員,而不需要例項化。在Ext JS 4 中也可以做到。如下程式碼所示:

Ext.onReady(function () {
    Ext.define('Computer', {
        statics: {
            instanceCount: 0,
            factory: function (brand) {
                //'this'在靜態方法中代表Computer類本身
                return new this({ brand: brand });
            }
        },
        config: {
            brand: null
        },
        constructor: function (config) {
            this.initConfig(config);
            //'self'屬性代表Computer類本身
            this.self.instanceCount++;
        }
    });

    //呼叫靜態函式建立兩個例項。
    var dellComputer = Computer.factory('Dell');
    var appleComputer = Computer.factory('Mac');

    //自動讀取config配置項的brand屬性。
    alert(appleComputer.getBrand()); // Alerts "Mac"
    //呼叫靜態屬性。
    alert(Computer.instanceCount); // Alerts "2"
});

他山之石 - Mixins

為了達到功能的複用,Ext JS 4中出現了混用的概念,即把某個類的功能"混"(Mixed)到另一個類之中。比如,我們要建立一個Musician類,讓他具有唱歌和彈吉他的功能,則可以這樣來實現:

Ext.onReady(function () {
    Ext.define('My.sample.Person', {
        name: 'Unknown',
        constructor: function (name) {
            if (name) {
                this.name = name;
            }
        },
        eat: function (foodType) {
            alert(this.name + " is eating: " + foodType);
        }
    });
            
    // 建立會唱歌的類
    Ext.define('My.sample.CanSing', {
        sing: function (songName) {
            alert("I'm singing " + songName);
        }
    });

    // 建立會彈吉他的類
    Ext.define('My.sample.CanPlayGuitar', {
        playGuitar: function () {
            alert("I'm playing guitar");
        }
    });

    Ext.define('My.sample.Musician', {
        extend: 'My.sample.Person',
        mixins: {
            //混入其他類中的功能
            canSing: 'My.sample.CanSing',
            canPlayGuitar: 'My.sample.CanPlayGuitar'
        }
    });

    var lily = Ext.create('My.sample.Musician', "Lily");
    //呼叫其他類中的方法
    lily.sing("November Rain"); // alerts "I'm singing November Rain"
    lily.playGuitar(); // alerts "I'm playing guitar"
});    

這裡,我們還可以對My.sample.CanSing中的sing方法進行重寫實現Musician自己的sing方法,只需要在Musician類中加上自己的sing配置項:

...
sing: function () {
    alert("Attention!");
    // this.mixins是指向所有mixins配置項的引用
    return this.mixins.canSing.sing.apply(this, arguments);
}
...

注意,這裡為了演示我把所有類的程式碼放在一起寫。在實際專案開發時,遵照Ext的命名約定,每個類應該根據全名放在特定的資料夾和檔案中,如上面的My.sample.Person類存放的檔案應該是My/sample/Person.js,My.sample.CanSing類存放的檔案應該是My/sample/CanSing.js ,而Ext.onReady(...)中的內容應該放在和實際的業務頁面名稱相同的js檔案中。

結束語

Ext JS 4 是一個含有300多個類的框架,它具有快速開發、可擴充套件,容易維護等優點。Ext JS 4 框架讓開發者用他們熟悉物件導向方法進行開發。除了本文介紹的,Ext JS 4還有很多吸引人的功能,我將會在後面的博文中進行介紹。

 

參考:
http://docs.sencha.com/extjs/4.0.7/#!/guide/class_system

相關文章