使用Modello編寫(xiě)JavaScript類
From:http://www.ajaxwing.com/index.php?id=2
一,背景
回顧一下編程語(yǔ)言的發(fā)展,不難發(fā)現(xiàn)這是一個(gè)不斷封裝的過(guò)程:從最開(kāi)始的匯編語(yǔ)言,到面向過(guò)程語(yǔ)言,然后到面向?qū)ο笳Z(yǔ)言,再到具備面向?qū)ο筇匦缘哪_本語(yǔ)言,一層一層封裝,一步一步減輕程序員的負(fù)擔(dān),逐漸提高編寫(xiě)程序的效率。這篇文章是關(guān)于 JavaScript 的,所以我們先來(lái)了解一下 JavaScript 是一種怎樣的語(yǔ)言。到目前為止,JavaScript 是一種不完全支持面向?qū)ο筇匦缘哪_本語(yǔ)言。之所以這樣說(shuō)是因?yàn)?nbsp;JavaScript 的確支持對(duì)象的概念,在程序中我們看到都是對(duì)象,可是 Javascipt 并不支持類的封裝和繼承。曾經(jīng)有過(guò) C++、Java或者 php、python 編程經(jīng)驗(yàn)的讀者都會(huì)知道,這些語(yǔ)言允許我們使用類來(lái)設(shè)計(jì)對(duì)象,并且這些類是可繼承的。JavaScript 的確支持自定義對(duì)象和繼承,不過(guò)使用的是另外一種方式:prototype(中文譯作:原型)。用過(guò) JavaScript 的或者讀過(guò)《設(shè)計(jì)模式》的讀者都會(huì)了解這種技術(shù),描述如下:
每個(gè)對(duì)象都包含一個(gè) prototype 對(duì)象,當(dāng)向?qū)ο蟛樵円粋€(gè)屬性或者請(qǐng)求一個(gè)方法的時(shí)候,運(yùn)行環(huán)境會(huì)先在當(dāng)前對(duì)象中查找,如果查找失敗則查找其 prototype 對(duì)象。注意 prototype 也是一個(gè)對(duì)象,于是這種查找過(guò)程同樣適用在對(duì)象的 prototype 對(duì)象中,直到當(dāng)前對(duì)象的 prototpye 為空。
在 JavaScript 中,對(duì)象的 prototype 在運(yùn)行期是不可見(jiàn)的,只能在定義對(duì)象的構(gòu)造函數(shù)時(shí),創(chuàng)建對(duì)象之前設(shè)定。下面的用法都是錯(cuò)誤的:
o2.prototype = o1;
/*
這時(shí)只定義了 o2 的一個(gè)名為“prototype”的屬性,
并沒(méi)有將 o1 設(shè)為 o2 的 prototype。
*/
// ---------------
f2 = function(){};
o2 = new f2;
f2.prototype = o1;
/*
這時(shí) o1 并沒(méi)有成為 o2 的 prototype,
因?yàn)?nbsp;o2 在 f2 設(shè)定 prototype 之前已經(jīng)被創(chuàng)建。
*/
// ---------------
f1 = function(){};
f2 = function(){};
o1 = new f1;
f2.prototype = o1;
o2 = new f2;
/*
同樣,這時(shí) o1 并不是 o2 的 prototype,
因?yàn)?nbsp;JavaScript 不允許構(gòu)造函數(shù)的 prototype 對(duì)象被其它變量直接引用。
*/
正確的用法應(yīng)該是:
f1 = function(){};
f2 = function(){};
f2.prototype = new f1;
o2 = new f2;
從上面的例子可以看出:如果你想讓構(gòu)造函數(shù) F2 繼承另外一個(gè)構(gòu)造函數(shù) F1 所定義的屬性和方法,那么你必須先創(chuàng)建一個(gè) F1 的實(shí)例對(duì)象,并立刻將其設(shè)為 F2 的 prototype。于是你會(huì)發(fā)現(xiàn)使用 prototype 這種繼承方法實(shí)際上是不鼓勵(lì)使用繼承:一方面是由于 JavaScript 被設(shè)計(jì)成一種嵌入式腳本語(yǔ)言,比方說(shuō)嵌入到瀏覽器中,用它編寫(xiě)的應(yīng)用一般不會(huì)很大很復(fù)雜,不需要用到繼承;另一方面如果繼承得比較深,prototype 鏈就會(huì)比較長(zhǎng),用在查找對(duì)象屬性和方法的時(shí)間就會(huì)變長(zhǎng),降低程序的整體運(yùn)行效率。
二,問(wèn)題
現(xiàn)在 JavaScript 的使用場(chǎng)合越來(lái)越多,web2.0 有一個(gè)很重要的方面就是用戶體驗(yàn)。好的用戶體驗(yàn)不但要求美工做得好,并且講求響應(yīng)速度和動(dòng)態(tài)效果。很多有名的 web2.0 應(yīng)用都使用了大量的 JavaScript 代碼,比方說(shuō) Flickr、Gmail 等等。甚至有些人用 Javasript 來(lái)編寫(xiě)基于瀏覽器的 GUI,比方說(shuō) Backbase、Qooxdoo 等等。于是 JavaScript 代碼的開(kāi)發(fā)和維護(hù)成了一個(gè)很重要的問(wèn)題。很多人都不喜歡自己發(fā)明輪子,他們希望 JavaScript 可以像其它編程語(yǔ)言一樣,有一套成熟穩(wěn)定 Javasript 庫(kù)來(lái)提高他們的開(kāi)發(fā)速度和效率。更多人希望的是,自己所寫(xiě)的 JavaScript 代碼能夠像其它面向?qū)ο笳Z(yǔ)言寫(xiě)的代碼一樣,具有很好的模塊化特性和很好的重用性,這樣維護(hù)起來(lái)會(huì)更方便??墒乾F(xiàn)在的 JavaScript 并沒(méi)有很好的支持這些需求,大部分開(kāi)發(fā)都要重頭開(kāi)始,并且維護(hù)起來(lái)很不方便。
三,已有解決方案
有需求自然就會(huì)有解決方案,比較成熟的有兩種:
1,現(xiàn)在很多人在自己的項(xiàng)目中使用一套叫 prototype.js 的 JavaScript 庫(kù),那是由 MVC web 框架 Ruby on Rails 開(kāi)發(fā)并使用 JavaScript 基礎(chǔ)庫(kù)。這套庫(kù)設(shè)計(jì)精良并且具有很好的可重用性和跨瀏覽器特性,使用 prototype.js 可以大大簡(jiǎn)化客戶端代碼的開(kāi)發(fā)工作。prototype.js 引入了類的概念,用其編寫(xiě)的類可以定義一個(gè) initialize 的初始化函數(shù),在創(chuàng)建類實(shí)例的時(shí)候會(huì)首先調(diào)用這個(gè)初始化函數(shù)。正如其名字,prototype.js 的核心還是 prototype,雖然提供了很多可復(fù)用的代碼,但沒(méi)有從根本上解決 JavaScript 的開(kāi)發(fā)和維護(hù)問(wèn)題。
2,使用 asp.net 的人一般都會(huì)聽(tīng)過(guò)或者用到一個(gè)叫 Atlas 的框架,那是微軟的 AJAX 利器。Atlas 允許客戶端代碼用類的方法來(lái)編寫(xiě),并且比 prototype.js 具備更好的面向?qū)ο筇匦裕确秸f(shuō)定義類的私有屬性和私有方法、支持繼承、像java那樣編寫(xiě)接口等等。Atlas 是一個(gè)從客戶端到服務(wù)端的解決方案,但只能在 asp.net 中使用、版權(quán)等問(wèn)題限制了其使用范圍。
從根本上解決問(wèn)題只有一個(gè),就是等待 JavaScript2.0(或者說(shuō)ECMAScript4.0)標(biāo)準(zhǔn)的出臺(tái)。在下一版本的 JavaScript 中已經(jīng)從語(yǔ)言上具備面向?qū)ο蟮奶匦浴A硗?,微軟?nbsp;JScript.NET 已經(jīng)可以使用這些特性。當(dāng)然,等待不是一個(gè)明智的方法。
四,Modello 框架
如果上面的表述讓你覺(jué)得有點(diǎn)頭暈,最好不要急于了解 Modello 框架,先保證這幾個(gè)概念你已經(jīng)能夠準(zhǔn)確理解:
JavaScript 構(gòu)造函數(shù):在 JavaScript 中,自定義對(duì)象通過(guò)構(gòu)造函數(shù)來(lái)設(shè)計(jì)。運(yùn)算符 new 加上構(gòu)造函數(shù)就會(huì)創(chuàng)建一個(gè)實(shí)例對(duì)象
JavaScript 中的 prototype:如果將一個(gè)對(duì)象 P 設(shè)定為一個(gè)構(gòu)造函數(shù) F 的 prototype,那么使用 F 創(chuàng)建的實(shí)例對(duì)象就會(huì)繼承 P 的屬性和方法
類:面向?qū)ο笳Z(yǔ)言使用類來(lái)封裝和設(shè)計(jì)對(duì)象。按類型分,類的成員分為屬性和方法。按訪問(wèn)權(quán)限分,類的成員分為靜態(tài)成員,私有成員,保護(hù)成員,公有成員
類的繼承:面向?qū)ο笳Z(yǔ)言允許一個(gè)類繼承另外一個(gè)類的屬性和方法,繼承的類叫做子類,被繼承的類叫做父類。某些語(yǔ)言允許一個(gè)子類只能繼承一個(gè)父類(單繼承),某些語(yǔ)言則允許繼承多個(gè)(多繼承)
JavaScript 中的 closure 特性:函數(shù)的作用域就是一個(gè) closure。JavaScript 允許在函數(shù) O 中定義內(nèi)部函數(shù) I ,內(nèi)部函數(shù) I 總是可以訪問(wèn)其外部函數(shù) O 中定義的變量。即使在外部函數(shù) O 返回之后,你再調(diào)用內(nèi)部函數(shù) I ,同樣可以訪問(wèn)外部函數(shù) O 中定義的變量。也就是說(shuō),如果你在構(gòu)造函數(shù) C 中用 var 定義了一個(gè)變量V,用 this 定義了一個(gè)函數(shù)F,由 C 創(chuàng)建的實(shí)例對(duì)象 O 調(diào)用 O.F 時(shí),F(xiàn) 總是可以訪問(wèn)到 V,但是用 O.V 這樣來(lái)訪問(wèn)卻不行,因?yàn)?nbsp;V 不是用 this 來(lái)定義的。換言之,V 成了 O 的私有成員。這個(gè)特性非常重要,如果你還沒(méi)有徹底搞懂,請(qǐng)參考這篇文章《Private Members in JavaScript》
搞懂上面的概念,理解下面的內(nèi)容對(duì)你來(lái)說(shuō)已經(jīng)沒(méi)有難度,開(kāi)始吧!
如題,Modello 是一個(gè)允許并且鼓勵(lì)你用 JavaScript 來(lái)編寫(xiě)類的框架。傳統(tǒng)的 JavaScript 使用構(gòu)造函數(shù)來(lái)自定義對(duì)象,用 prototype 來(lái)實(shí)現(xiàn)繼承。在 Modello 中,你可以忘掉晦澀的 prototype,因?yàn)?nbsp;Modello 使用類來(lái)設(shè)計(jì)對(duì)象,用類來(lái)實(shí)現(xiàn)繼承,就像其它面向?qū)ο笳Z(yǔ)言一樣,并且使用起來(lái)更加簡(jiǎn)單。不信嗎?請(qǐng)繼續(xù)往下看。
使用 Modello 編寫(xiě)的類所具備如下特性:
私有成員、公共成員和靜態(tài)成員
類的繼承,多繼承
命名空間
類型鑒別
Modello 還具有以下特性:
更少的概念,更方便的使用方法
小巧,只有兩百行左右的代碼
設(shè)計(jì)期和運(yùn)行期徹底分離,使用繼承的時(shí)候不需要使用 prototype,也不需要先創(chuàng)建父類的實(shí)例
兼容 prototype.js 的類,兼容 JavaScript 構(gòu)造函數(shù)
跨瀏覽器,跨瀏覽器版本
開(kāi)放源代碼,BSD licenced,允許免費(fèi)使用在個(gè)人項(xiàng)目或者商業(yè)項(xiàng)目中
下面介紹 Modello 的使用方法:
1,定義一個(gè)類
Point = Class.create();
/*
創(chuàng)建一個(gè)類。用過(guò) prototype.js 的人覺(jué)得很熟悉吧;)
*/
2,注冊(cè)一個(gè)類
Point.register("Modello.Point");
/*
這里"Modello"是命名空間,"Point"是類名,之間用"."分隔
如果注冊(cè)成功,
Point.namespace 等于 "Modello",Point.classname 等于 "Point"。
如果失敗 Modello 會(huì)拋出一個(gè)異常,說(shuō)明失敗原因。
*/
Point.register("Point"); // 這里使用默認(rèn)的命名空間 "std"
Class.register(Point, "Point"); // 使用 Class 的 register 方法
3,獲取已注冊(cè)的類
P = Class.get("Modello.Point");
P = Class.get("Point"); // 這里使用默認(rèn)的命名空間 "std"
4,使用繼承
ZPoint = Class.create(Point); // ZPoint 繼承 Point
ZPoint = Class.create("Modello.Point"); // 繼承已注冊(cè)的類
ZPoint = Class.create(Point1, Point2[, ...]);
/*
多繼承。參數(shù)中的類也可以用已注冊(cè)的類名來(lái)代替
*/
/*
繼承關(guān)系:
Point.subclasses 內(nèi)容為 [ ZPoint ]
ZPoint.superclasses 內(nèi)容為 [ Point ]
*/
5,定義類的靜態(tài)成員
Point.count = 0;
Point.add = function(x, y) {
return x + y;
}
6,定義類的構(gòu)造函數(shù)
Point.construct = function($self, $class) {
// 用 "var" 來(lái)定義私有成員
var _name = "";
var _getName = function () {
return _name;
}
// 用 "this" 來(lái)定義公有成員
this.x = 0;
this.y = 0;
this.initialize = function (x, y) { // 初始化函數(shù)
this.x = x;
this.y = y;
$class.count += 1; // 訪問(wèn)靜態(tài)成員
// 公有方法訪問(wèn)私有私有屬性
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() {
// 調(diào)用靜態(tài)方法,使用構(gòu)造函數(shù)傳入的 $class
return $class.add(this.x, this.y);
}
}
ZPoint.construct = function($self, $class) {
this.z = 0; // this.x, this.y 繼承自 Point
// 重載 Point 的初始化函數(shù)
this.initialize = function (x, y, z) {
this.z = z;
// 調(diào)用第一個(gè)父類的初始化函數(shù),
// 第二個(gè)父類是 $self.super1,如此類推。
// 注意:這里使用的是構(gòu)造函數(shù)傳入的 $self 變量
$self.super0.initialize.call(this, x, y);
// 調(diào)用父類的任何方法都可以使用這種方式,但只限于父類的公有方法
}
// 重載 Point 的 toString 方法
this.toString = function () {
return "Point(" + this.x + ", " + this.y +
", " + this.z + ")";
}
}
// 連寫(xiě)技巧
Class.create().register("Modello.Point").construct = function($self, $class) {
// ...
}
7,創(chuàng)建類的實(shí)例
// 兩種方法: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
// 上面的類均可替換成已注冊(cè)的類名
以上就是 Modello 提供的全部功能。下面說(shuō)說(shuō)使用 Modello 的注意事項(xiàng)和建議:
在使用繼承時(shí),傳入的父類可以是使用 prototype.js 方式定義的類或者 JavaScript 方式定義的構(gòu)造函數(shù)
類實(shí)際上也是一個(gè)函數(shù),普通的 prototype 的繼承方式同樣適用在用 Modello 定義的類中
類可以不注冊(cè),這種類叫做匿名類,不能通過(guò) Class.get 方法獲取
如果定義類構(gòu)造函數(shù)時(shí),像上面例子那樣提供了 $self, $class 兩個(gè)參數(shù),Modello 會(huì)在創(chuàng)建實(shí)例時(shí)將實(shí)例本身傳給 $self,將類本身傳給 $class。$self 一般在訪問(wèn)父類成員時(shí)才使用,$class 一般在訪問(wèn)靜態(tài)成員時(shí)才使用。雖然 $self和$class 功能很強(qiáng)大,但不建議你在其它場(chǎng)合使用,除非你已經(jīng)讀懂 Modello 的源代碼,并且的確有特殊需求。更加不要嘗試使用 $self 代替 this,這樣可能會(huì)給你帶來(lái)麻煩
子類無(wú)法訪問(wèn)父類的私有成員,靜態(tài)方法中無(wú)法訪問(wèn)私有成員
Modello 中私有成員的名稱沒(méi)有特別限制,不過(guò)用"_"開(kāi)始是一個(gè)好習(xí)慣
Modello 不支持保護(hù)(protected)成員,如果你想父類成員可以被子類訪問(wèn),則必須將父類成員定義為公有。你也可以參考 "this._property" 這樣的命名方式來(lái)表示保護(hù)成員:)
盡量將一些輔助性的計(jì)算復(fù)雜度大的方法定義成靜態(tài)成員,這樣可以提高運(yùn)行效率
使用 Modello 的繼承和類型鑒別可以實(shí)現(xiàn)基本的接口(interface)功能,你已經(jīng)發(fā)現(xiàn)這一點(diǎn)了吧;)
使用多繼承的時(shí)候,左邊的父類優(yōu)先級(jí)高于右邊的父類。也就是說(shuō)假如多個(gè)父類定義了同一個(gè)方法,最左邊的父類定義的方法最終被繼承
使用 Modello 編寫(xiě)的類功能可以媲美使用 Atlas 編寫(xiě)的類,并且使用起來(lái)更簡(jiǎn)潔。如果你想用 Modello 框架代替 prototype.js 中的簡(jiǎn)單類框架,只需要先包含 modello.js,然后去掉 prototype.js 中定義 Class 的幾行代碼即可,一切將正常運(yùn)行。
如果你發(fā)現(xiàn) Modello 的 bug,非常歡迎你通過(guò) email 聯(lián)系我。如果你覺(jué)得 Modello 應(yīng)該具備更多功能,你可以嘗試閱讀一下源代碼,你會(huì)發(fā)現(xiàn) Modello 可以輕松擴(kuò)展出你所需要的功能。
Modello 的原意為“大型藝術(shù)作品的模型”,希望 Modello 能夠幫助你編寫(xiě)高質(zhì)量的 JavaScript 代碼。
5,下載
Modello 的完整參考說(shuō)明和下載地址:http://modello.sourceforge.net
相關(guān)文章
ES6?class類實(shí)現(xiàn)繼承實(shí)例詳解
傳統(tǒng)的javascript中只有對(duì)象,沒(méi)有類的概念,下面這篇文章主要給大家介紹了關(guān)于ES6?class類實(shí)現(xiàn)繼承的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10JS獲取子窗口中返回的數(shù)據(jù)實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇JS獲取子窗口中返回的數(shù)據(jù)實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-05-05JavaScript遞歸函數(shù)解“漢諾塔”算法代碼解析
這篇文章主要介紹了JavaScript遞歸函數(shù)解“漢諾塔”算法代碼解析,需要的朋友可以參考下2018-07-07微信公眾平臺(tái)開(kāi)發(fā)教程(四) 實(shí)例入門(mén):機(jī)器人回復(fù)(附源碼)
本篇文章主要介紹了微信公眾平臺(tái)開(kāi)發(fā)機(jī)器人,可以實(shí)現(xiàn)簡(jiǎn)單對(duì)話和查詢天氣等,有需要的可以了解一下。2016-12-12