使用Modello編寫JavaScript類_javascript技巧

來源:互聯網
上載者:User

From:http://www.ajaxwing.com/index.php?id=2


一,背景
回顧一下程式設計語言的發展,不難發現這是一個不斷封裝的過程:從最開始的組合語言,到面向過程語言,然後到物件導向語言,再到具備物件導向特性的指令碼語言,一層一層封裝,一步一步減輕程式員的負擔,逐漸提高編寫程式的效率。這篇文章是關於 JavaScript 的,所以我們先來瞭解一下 JavaScript 是一種怎樣的語言。到目前為止,JavaScript 是一種不完全支援物件導向特性的指令碼語言。之所以這樣說是因為 JavaScript 的確支援對象的概念,在程式中我們看到都是對象,可是 Javascipt 並不支援類的封裝和繼承。曾經有過 C++、Java或者 php、python 編程經驗的讀者都會知道,這些語言允許我們使用類來設計對象,並且這些類是可繼承的。JavaScript 的確支援自訂對象和繼承,不過使用的是另外一種方式:prototype(中文譯作:原型)。用過 JavaScript 的或者讀過《設計模式》的讀者都會瞭解這種技術,描述如下:

每個對象都包含一個 prototype 對象,當向物件查詢一個屬性或者請求一個方法的時候,運行環境會先在當前對象中尋找,如果尋找失敗則尋找其 prototype 對象。注意 prototype 也是一個對象,於是這種尋找過程同樣適用在對象的 prototype 對象中,直到當前對象的 prototpye 為空白。

在 JavaScript 中,對象的 prototype 在運行期是不可見的,只能在定義對象的建構函式時,建立對象之前設定。下面的用法都是錯誤的:

o2.prototype = o1;
/*
  這時只定義了 o2 的一個名為“prototype”的屬性,
  並沒有將 o1 設為 o2 的 prototype。
*/

// ---------------

f2 = function(){};
o2 = new f2;
f2.prototype = o1;
/*
  這時 o1 並沒有成為 o2 的 prototype,
  因為 o2 在 f2 設定 prototype 之前已經被建立。
*/

// ---------------

f1 = function(){};
f2 = function(){};
o1 = new f1;
f2.prototype = o1;
o2 = new f2;
/*
  同樣,這時 o1 並不是 o2 的 prototype,
  因為 JavaScript 不允許建構函式的 prototype 對象被其它變數直接引用。
*/

正確的用法應該是:

f1 = function(){};
f2 = function(){};
f2.prototype = new f1;
o2 = new f2;

從上面的例子可以看出:如果你想讓建構函式 F2 繼承另外一個建構函式 F1 所定義的屬性和方法,那麼你必須先建立一個 F1 的執行個體對象,並立刻將其設為 F2 的 prototype。於是你會發現使用 prototype 這種繼承方法實際上是不鼓勵使用繼承:一方面是由於 JavaScript 被設計成一種嵌入式指令碼語言,比方說嵌入到瀏覽器中,用它編寫的應用一般不會很大很複雜,不需要用到繼承;另一方面如果繼承得比較深,prototype 鏈就會比較長,用在尋找對象屬性和方法的時間就會變長,降低程式的整體運行效率。

二,問題
現在 JavaScript 的使用場合越來越多,web2.0 有一個很重要的方面就是使用者體驗。好的使用者體驗不但要求美工做得好,並且講求響應速度和動態效果。很多有名的 web2.0 應用都使用了大量的 JavaScript 代碼,比方說 Flickr、Gmail 等等。甚至有些人用 Javasript 來編寫基於瀏覽器的 GUI,比方說 Backbase、Qooxdoo 等等。於是 JavaScript 代碼的開發和維護成了一個很重要的問題。很多人都不喜歡自己發明輪子,他們希望 JavaScript 可以像其它程式設計語言一樣,有一套成熟穩定 Javasript 庫來提高他們的開發速度和效率。更多人希望的是,自己所寫的 JavaScript 代碼能夠像其它物件導向語言寫的代碼一樣,具有很好的模組化特性和很好的重用性,這樣維護起來會更方便。可是現在的 JavaScript 並沒有很好的支援這些需求,大部分開發都要重頭開始,並且維護起來很不方便。

三,已有解決方案
有需求自然就會有解決方案,比較成熟的有兩種:

1,現在很多人在自己的項目中使用一套叫 prototype.js 的 JavaScript 庫,那是由 MVC web 架構 Ruby on Rails 開發並使用 JavaScript 基礎庫。這套庫設計精良並且具有很好的可重用性和跨瀏覽器特性,使用 prototype.js 可以大大簡化用戶端代碼的開發工作。prototype.js 引入了類的概念,用其編寫的類可以定義一個 initialize 的初始化函數,在建立類執行個體的時候會首先調用這個初始化函數。正如其名字,prototype.js 的核心還是 prototype,雖然提供了很多可複用的代碼,但沒有從根本上解決 JavaScript 的開發和維護問題。

2,使用 asp.net 的人一般都會聽過或者用到一個叫 Atlas 的架構,那是微軟的 AJAX 利器。Atlas 允許用戶端代碼用類的方法來編寫,並且比 prototype.js 具備更好的物件導向特性,比方說定義類的私人屬性和私人方法、支援繼承、像java那樣編寫介面等等。Atlas 是一個從用戶端到服務端的解決方案,但只能在 asp.net 中使用、著作權等問題限制了其使用範圍。

從根本上解決問題只有一個,就是等待 JavaScript2.0(或者說ECMAScript4.0)標準的出台。在下一版本的 JavaScript 中已經從語言上具備物件導向的特性。另外,微軟的 JScript.NET 已經可以使用這些特性。當然,等待不是一個明智的方法。

四,Modello 架構
如果上面的表述讓你覺得有點頭暈,最好不要急於瞭解 Modello 架構,先保證這幾個概念你已經能夠準確理解:

JavaScript 建構函式:在 JavaScript 中,自訂對象通過建構函式來設計。運算子 new 加上建構函式就會建立一個執行個體對象 
JavaScript 中的 prototype:如果將一個對象 P 設定為一個建構函式 F 的 prototype,那麼使用 F 建立的執行個體對象就會繼承 P 的屬性和方法 
類:物件導向語言使用類來封裝和設計對象。按類型分,類的成員分為屬性和方法。按存取權限分,類的成員分為靜態成員,私人成員,保護成員,公有成員 
類的繼承:物件導向語言允許一個類繼承另外一個類的屬性和方法,繼承的類叫做子類,被繼承的類叫做父類。某些語言允許一個子類只能繼承一個父類(單繼承),某些語言則允許繼承多個(多繼承) 
JavaScript 中的 closure 特性:函數的範圍就是一個 closure。JavaScript 允許在函數 O 中定義內建函式 I ,內建函式 I 總是可以訪問其外部函數 O 中定義的變數。即使在外部函數 O 返回之後,你再調用內建函式 I ,同樣可以訪問外部函數 O 中定義的變數。也就是說,如果你在建構函式 C 中用 var 定義了一個變數V,用 this 定義了一個函數F,由 C 建立的執行個體對象 O 調用 O.F 時,F 總是可以訪問到 V,但是用 O.V 這樣來訪問卻不行,因為 V 不是用 this 來定義的。換言之,V 成了 O 的私人成員。這個特性非常重要,如果你還沒有徹底搞懂,請參考這篇文章《Private Members in JavaScript》 
搞懂上面的概念,理解下面的內容對你來說已經沒有難度,開始吧!

如題,Modello 是一個允許並且鼓勵你用 JavaScript 來編寫類的架構。傳統的 JavaScript 使用建構函式來自訂對象,用 prototype 來實現繼承。在 Modello 中,你可以忘掉晦澀的 prototype,因為 Modello 使用類來設計對象,用類來實現繼承,就像其它物件導向語言一樣,並且使用起來更加簡單。不信嗎?請繼續往下看。

使用 Modello 編寫的類所具備如下特性:

私人成員、公用成員和靜態成員 
類的繼承,多繼承 
命名空間 
類型鑒別 
Modello 還具有以下特性:

更少的概念,更方便的使用方法 
小巧,只有兩百行左右的代碼 
設計期和運行期徹底分離,使用繼承的時候不需要使用 prototype,也不需要先建立父類的執行個體 
相容 prototype.js 的類,相容 JavaScript 建構函式 
跨瀏覽器,跨瀏覽器版本 
開放原始碼,BSD licenced,允許免費使用在個人項目或者商業項目中 
下面介紹 Modello 的使用方法:

1,定義一個類

Point = Class.create();
/*
  建立一個類。用過 prototype.js 的人覺得很熟悉吧;)
*/

2,註冊一個類

Point.register("Modello.Point");
/*
  這裡"Modello"是命名空間,"Point"是類名,之間用"."分隔
  如果註冊成功,
  Point.namespace 等於 "Modello",Point.classname 等於 "Point"。
  如果失敗 Modello 會拋出一個異常,說明失敗原因。
*/

Point.register("Point"); // 這裡使用預設的命名空間 "std"

Class.register(Point, "Point"); // 使用 Class 的 register 方法

3,擷取登入的類

P = Class.get("Modello.Point");
P = Class.get("Point"); // 這裡使用預設的命名空間 "std"

4,使用繼承

ZPoint = Class.create(Point); // ZPoint 繼承 Point

ZPoint = Class.create("Modello.Point"); // 繼承登入的類

ZPoint = Class.create(Point1, Point2[, ...]);
/*
  多繼承。參數中的類也可以用登入的類名來代替
*/

/*
  繼承關係:
  Point.subclasses 內容為 [ ZPoint ]
  ZPoint.superclasses 內容為 [ Point ]
*/

5,定義類的靜態成員

Point.count = 0;
Point.add = function(x, y) {
    return x + y;
}

6,定義類的建構函式

Point.construct = function($self, $class) {

    // 用 "var" 來定義私人成員
    var _name = "";
    var _getName = function () {
        return _name;
    }

    // 用 "this" 來定義公有成員
    this.x = 0;
    this.y = 0;
    this.initialize = function (x, y) { // 初始化函數
        this.x = x;
        this.y = y;
        $class.count += 1; // 訪問靜態成員

    // 公有方法訪問私人私人屬性
    this.setName = function (name) {
        _name = name;
    }

    this.getName = function () {
        return _getName();
    }

    this.toString = function () {
        return "Point(" + this.x + ", " + this.y + ")";
    }
    // 注意:initialize 和 toString 方法只有定義成公有成員才生效

    this.add = function() {
        // 調用靜態方法,使用建構函式傳入的 $class
        return $class.add(this.x, this.y);
    }

}

ZPoint.construct = function($self, $class) {

    this.z = 0; // this.x, this.y 繼承自 Point

    // 重載 Point 的初始化函數
    this.initialize = function (x, y, z) {
        this.z = z;
        // 調用第一個父類的初始化函數,
        // 第二個父類是 $self.super1,如此類推。
        // 注意:這裡使用的是建構函式傳入的 $self 變數
        $self.super0.initialize.call(this, x, y);
        // 調用父類的任何方法都可以使用這種方式,但只限於父類的公有方法
    }

    // 重載 Point 的 toString 方法
    this.toString = function () {
        return "Point(" + this.x + ", " + this.y +
               ", " + this.z + ")";
    }

}

// 連寫技巧
Class.create().register("Modello.Point").construct = function($self, $class) {
    // ...
}

7,建立類的執行個體

// 兩種方法:new 和 create
point = new Point(1, 2);
point = Point.create(1, 2);
point = Class.get("Modello.Point").create(1, 2);
zpoint = new ZPoint(1, 2, 3);

8,類型鑒別

ZPoint.subclassOf(Point); // 返回 true
point.instanceOf(Point); // 返回 true
point.isA(Point); // 返回 true
zpoint.isA(Point); // 返回 true
zpoint.instanceOf(Point); // 返回 false
// 上面的類均可替換成登入的類名

以上就是 Modello 提供的全部功能。下面說說使用 Modello 的注意事項和建議:

在使用繼承時,傳入的父類可以是使用 prototype.js 方式定義的類或者 JavaScript 方式定義的建構函式 
類實際上也是一個函數,普通的 prototype 的繼承方式同樣適用在用 Modello 定義的類中 
類可以不註冊,這種類叫做匿名類,不能通過 Class.get 方法擷取 
如果定義類建構函式時,像上面例子那樣提供了 $self, $class 兩個參數,Modello 會在建立執行個體時將執行個體本身傳給 $self,將類本身傳給 $class。$self 一般在訪問父類成員時才使用,$class 一般在訪問靜態成員時才使用。雖然 $self和$class 功能很強大,但不建議你在其它場合使用,除非你已經讀懂 Modello 的原始碼,並且的確有特殊需求。更加不要嘗試使用 $self 代替 this,這樣可能會給你帶來麻煩 
子類無法訪問父類的私人成員,靜態方法中無法訪問私人成員 
Modello 中私人成員的名稱沒有特別限制,不過用"_"開始是一個好習慣 
Modello 不支援保護(protected)成員,如果你想父類成員可以被子類訪問,則必須將父類成員定義為公有。你也可以參考 "this._property" 這樣的命名方式來表示保護成員:) 
盡量將一些輔助性的計算複雜度大的方法定義成靜態成員,這樣可以提高運行效率 
使用 Modello 的繼承和類型鑒別可以實現基本的介面(interface)功能,你已經發現這一點了吧;) 
使用多繼承的時候,左邊的父類優先順序高於右邊的父類。也就是說假如多個父類定義了同一個方法,最左邊的父類定義的方法最終被繼承 
使用 Modello 編寫的類功能可以媲美使用 Atlas 編寫的類,並且使用起來更簡潔。如果你想用 Modello 架構代替 prototype.js 中的簡單類架構,只需要先包含 modello.js,然後去掉 prototype.js 中定義 Class 的幾行代碼即可,一切將正常運行。


如果你發現 Modello 的 bug,非常歡迎你通過 email 聯絡我。如果你覺得 Modello 應該具備更多功能,你可以嘗試閱讀一下原始碼,你會發現 Modello 可以輕鬆擴充出你所需要的功能。

Modello 的原意為“大型藝術作品的模型”,希望 Modello 能夠協助你編寫高品質的 JavaScript 代碼。

5,下載
Modello 的完整參考說明和下載地址:http://modello.sourceforge.net

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.