面向?qū)ο缶幊趟枷朐谔岢鲋?,很快就流行起來了,它將開發(fā)人員從冗長,繁復(fù),難以調(diào)試的過程式程序中解放了出來,過程式語 言如 C ,代碼的形式往往如此:
C代碼
Component comp;
init_component(& comp, props);
而面向?qū)ο蟮恼Z言如 Java ,則會是這種形式:
Component comp;
comp.init(props);
可以看出,方法是對象的方法,對象是方法的對象,這樣的代碼形式更接近人的思維方式,因此 OO 大行其道也并非僥幸。
JavaScript 本身是基于對象 的,而并非基于類。但是, JavaScript 的函數(shù)式語言的特性使得它本身是可編程 的,它可以變成你想要的任何形式。我們在這一章詳細討論如何使用 JavaScript 進行 OO 風(fēng)格的代碼開發(fā)。
JavaScript 中的繼承可以通過原型鏈來實現(xiàn),調(diào)用對象上的一個方法,由于方法在 JavaScript 對象中是對另一個函數(shù)對象的引用,因此解釋器會在對象中查找該屬性,如果沒有找到,則在其內(nèi)部對象 prototype 對象上搜索,由于 prototype 對象與對象本身的結(jié)構(gòu)是一樣的,因此這個過程會一直回溯到發(fā)現(xiàn)該屬性,則調(diào)用該屬性,否則,報告一個錯誤。關(guān)于原型繼承,我們不妨看一個小例 子:
function Base(){
this .baseFunc = function (){
print ( "base behavior" );
}
}
function Middle(){
this .middleFunc = function (){
print ( "middle behavior" );
}
}
Middle. prototype = new Base();
function Final(){
this .finalFunc = function (){
print ( "final behavior" );
}
}
Final. prototype = new Middle();
function test(){
var obj = new Final();
obj.baseFunc();
obj.middleFunc();
obj.finalFunc();
}
http://wiki.jikexueyuan.com/project/javascript-core/images/js10.png" alt="" />
圖 原型鏈的示意圖
在 function test 中,我們 new 了一個 Final 對象,然后依次調(diào)用 obj.baseFunc ,由于 obj 對象上并無此方法,則按照上邊提到的規(guī)則,進行回溯,在其原型鏈上搜索,由于 Final 的原型鏈上包含 Middle ,而 Middle 上又包含 Base ,因此會執(zhí)行這個方法,這樣就實現(xiàn)了類的繼承。
base behavior
middle behavior
final behavior
但是這種繼承形式與傳統(tǒng)的 OO 語言大相徑庭,初學(xué)者很難適應(yīng),我們后邊的章節(jié)會涉及到一個比較好的 JavaScript 的面向?qū)ο蠡A(chǔ)包 Base ,使用 Base 包,雖然編碼風(fēng)格上會和傳統(tǒng)的 OO 語言不同,但是讀者很快就會發(fā)現(xiàn)這種風(fēng)格的好處。
引用是一個比較有意思的主題,跟其他的語言不同的是, JavaScript 中的引用始終指向最終的對象,而并非引用本身,我們來看一個例子:
<strong>var obj = {}; // 空對象
var ref = obj; // 引用
obj. name = "objectA" ;
print ( ref . name ); //ref 跟著添加了 name 屬性
obj = [ "one" , "two" , "three" ]; //obj 指向了另一個對象 ( 數(shù)組對象 )
print ( ref . name ); //ref 還指向原來的對象
print (obj. length ); //3
print ( ref . length ); //undefined</strong>
運行結(jié)果如下:
objectA
objectA
3
undefined
obj 只是對一個匿名對象的引用,所以, ref 并非指向它,當(dāng) obj 指向另一個數(shù)組對象時 可以看到,引用 ref 并未改變,而始終指向這那個后來添加了 name 屬性的 " 空 " 對象 ”{}” 。理解這一點對后邊的內(nèi)容有很大的幫助。
再看這個例子:
<strong>var obj = {}; // 新建一個對象,并被 obj 引用
var ref1 = obj; //ref1 引用 obj, 事實上是引用 obj 引用的空對象
var ref2 = obj;
obj.func = "function" ;
print (ref1.func);
print (ref2.func);</strong>
聲明一個對象,然后用兩個引用來引用這個對象,然后修改原始的對象,注意這兩步的順序,運行之:
function
function
根據(jù)運行結(jié)果我們可以看出,在定義了引用之后,修改原始的那個對象會影響到其引用上,這一點也應(yīng)該注意。
有面向?qū)ο缶幊痰幕A(chǔ)有時會成為一種負擔(dān),比如看到 new 的時候, Java 程序員可能會認為這將會調(diào)用一個類的構(gòu)造器構(gòu)造一個新的對象出來,我們來看一個例子:
function Shape(type){
this .type = type || "rect" ;
this .calc = function (){
return "calc, " + this .type;
}
}
var triangle = new Shape( "triangle" );
print (triangle.calc());
var circle = new Shape( "circle" );
print (circle.calc());
運行結(jié)果如下:
calc, triangle
calc, circle
Java 程序員可能會覺得 Shape 就是一個類,然后 triangle , circle 即是 Shape 對應(yīng)的具體對象,而其實 JavaScript 并非如此工作的,罪魁禍首即為此 new 操作符。在 JavaScript 中,通過 new 操作符來作用與一個函數(shù),實質(zhì)上會發(fā)生這樣的動作:
首先,創(chuàng)建一個空對象,然后用函數(shù)的 apply 方法,將這個空對象傳入作為 apply 的第一個參數(shù),及上下文參數(shù)。這樣函數(shù)內(nèi)部的 this 將會被這個空的對象所替代:
var triangle = new Shape( "triangle" );
// 上一句相當(dāng)于下面的代碼
var triangle = {};
Shape.apply(triangle, [ "triangle" ]);
事實上,我們可以通過 JavaScript 的函數(shù)實現(xiàn)封裝,封裝的好處在于未經(jīng)授權(quán)的客戶代碼無法訪問到我們不公開的數(shù)據(jù),我們來看這個例子:
function Person(name){
//private variable
var address = "The Earth" ;
//public method
this .getAddress = function (){
return address;
}
//public variable
this .name = name;
}
//public
Person.prototype.getName = function (){
return this .name;
}
//public
Person.prototype.setName = function (name){
this .name = name;
}
首先聲明一個函數(shù),作為模板,用面向?qū)ο蟮男g(shù)語來講,就是一個類 。用 var 方式聲明的變量僅在類內(nèi)部可見,所以 address 為一個私有成員,訪問 address 的唯一方法是通過我們向外暴露的 getAddress 方法,而 get/setName ,均為原型鏈上的方法,因此為公開的。我們可以做個測試:
var jack = new Person( "jack" );
print(jack.name);//jack
print(jack.getName());//jack
print(jack.address);//undefined
print(jack.getAddress());//The Earth
直接通過 jack.address 來訪問 address 變量會得到 undefined 。我們只能通過 jack.getAddress 來訪問。這樣, address 這個成員就被封裝起來了。 另外需要注意的一點是,我們可以為類添加靜態(tài)成員,這個過程也很簡單,只需要為函數(shù)對象添加一個 屬性即可。比如:
function Person(name){
//private variable
var address = "The Earth" ;
//public method
this .getAddress = function (){
return address;
}
//public variable
this .name = name;
}
Person.TAG = "javascript-core" ;// 靜態(tài)變量
print(Person.TAG);
也就是說,我們在訪問 Person.TAG 時, 不需要實例化 Person 類。這與 傳統(tǒng)的面向?qū)ο笳Z言如 Java 中的靜態(tài)變量是一致的。
Base 是由 Dean Edwards 開發(fā)的一個 JavaScript 的 面向?qū)ο蟮幕A(chǔ)包, Base 本身很小,只有 140 行,但是這個很小的包對面向?qū)ο缶幊田L(fēng)格有很好的支持,支持類的定義,封裝,繼承,子類調(diào)用 父類的方法等,代碼的質(zhì)量也很高,而且很多項目都在使用 Base 作為底 層的支持。盡管如此, JavaScript 的面向?qū)ο箫L(fēng)格依然非常古 怪,并不可以完全和傳統(tǒng)的 OO 語言對等起來。
下面我們來看幾個基于 Base 的例子, 假設(shè)我們現(xiàn)在在開發(fā)一個任務(wù)系統(tǒng),我們需要抽象出一個類來表示任務(wù),對應(yīng)的,每個任務(wù)都可能會有一個監(jiān)聽器,當(dāng)任務(wù)執(zhí)行之后,需要通知監(jiān)聽器。我們首先定 義一個事件監(jiān)聽器的類,然后定義一個任務(wù)類:
var EventListener = Base.extend({
constructor : function(sense){
this.sense = sense;
},
sense : null,
handle : function(){
print(this.sense+" occured");
}
});
var Task = Base.extend({
constructor : function(name){
this.name = name;
},
name : null,
listener : null,
execute : function(){
print(this.name);
this.listener.handle();
},
setListener : function(listener){
this.listener = listener;
}
});
創(chuàng)建類的方式很簡單,需要給 Base.extend 方 法傳入一個 JSON 對象,其中可以有成員和方法。方法訪問自身的成員時 需要加 this 關(guān)鍵字。而每一個類都會有一個 constructor 的方法,即構(gòu)造方法。比如事件監(jiān)聽器類 (EventListener) 的構(gòu)造器需要傳入一個字符串,而任務(wù)類 (Task) 也需要傳入任務(wù)的名字來進行構(gòu)造。好了,既然我們已經(jīng)有了任務(wù)類和事件監(jiān)聽器類,我們 來實例化它們:
var printing = new Task("printing");
var printEventListener = new EventListener("printing");
printing.setListener(printEventListener);
printing.execute();
首先,創(chuàng)建一個新的 Task , 做打印工作,然后新建一個事件監(jiān)聽器,并將它注冊在新建的任務(wù)上,這樣,當(dāng)打印發(fā)生時,會通知監(jiān)聽器,監(jiān)聽器會做出相應(yīng)的判斷:
printing
printing occurred
既然有了基本的框架,我們就來使用這個框架,假設(shè)我們要從 HTTP 服務(wù)器上下載一個頁面,于是我們設(shè)計了一個新的任務(wù)類型,叫做 HttpRequester :
var HttpRequester = Task.extend({
constructor : function(name, host, port){
this.base(name);
this.host = host;
this.port = port;
},
host : "127.0.0.1",
port : 9527,
execute : function(){
print("["+this.name+"] request send to "+this.host+" of port "+this.port);
this.listener.handle();
}
});
HttpRequester 類繼承了 Task ,并且重載了 Task 類 的 execute 方法, setListener 方法的內(nèi)容與父類一致,因此不需要重載。
var requester = new HttpRequester("requester1", "127.0.0.1", 8752);
var listener = new EventListener("http_request");
requester.setListener(listener);
requester.execute();
我們新建一個 HttpRequester 任 務(wù),然后注冊上事件監(jiān)聽器,并執(zhí)行之:
[requester1] request send to 127.0.0.1 of port 8752
http_request occured
應(yīng)該注意到 HttpRequester 類 的構(gòu)造器中,有這樣一個語句:
this.base(name);
表示執(zhí)行父類的構(gòu)造器,即將 name 賦 值給父類的成員變量 name ,這樣在 HttpRequester 的實例中,我們就可以通過 this.name 來訪問這個成員了。這套機制簡直與在其他傳統(tǒng)的 OO 語言并無二致。同時, HttpRequester 類 的 execute 方法覆蓋了父類的 execute 方法,用面向?qū)ο蟮男g(shù)語來講,叫做重載。 在很多應(yīng)用中,有些對象不會每次都創(chuàng)建新的實例,而是使用一個固有的實例,比如提供數(shù)據(jù)源的服務(wù),報表渲染引擎,事件分發(fā) 器等,每次都實例化一個會有很大的開銷,因此人們設(shè)計出了單例模式,整個應(yīng)用的生命周期中,始終只有頂多一個實例存在。 Base 同樣可以模擬出這樣的能力:
var ReportEngine = Base.extend({
constructor : null,
run : function(){
//render the report
}
});
這一節(jié),我們通過學(xué)習(xí)一個面向?qū)ο蟮膶嵗齺韺?JavaScript 的面向?qū)ο筮M行更深入的理解,這個例子不能太復(fù)雜,涉及到的內(nèi)容也不能僅僅為繼承,多態(tài)等概念,如果那樣,會失去閱讀的樂趣,最好是在實例中穿插一些講解,則可以得到最好的效果。
本節(jié)要分析的實例為一個事件分發(fā)器(Event Dispatcher),本身來自于一個實際項目,但同時又比較小巧,我對其代碼做了部分修改,去掉了一些業(yè)務(wù)相關(guān)的部分。
事件分發(fā)器通常是跟UI聯(lián)系在一起的,UI中有多個組件,它們之間經(jīng)常需要互相通信,當(dāng)UI比較復(fù)雜,而頁面元素的組織又不夠清晰的時候,事件的處理會非常麻煩。在本節(jié)的例子中,事件分發(fā)器為一個對象,UI組件發(fā)出事件到事件分發(fā)器,也可以注冊自己到分發(fā)器,當(dāng)自己關(guān)心的事件到達時,進行響應(yīng)。如果你熟悉設(shè)計模式的話,會很快想到觀察者模式,例子中的事件分發(fā)器正式使用了此模式。
var uikit = uikit || {};
uikit.event = uikit.event || {};
uikit.event.EventTypes = {
EVENT_NONE : 0,
EVENT_INDEX_CHANGE : 1,
EVENT_LIST_DATA_READY : 2,
EVENT_GRID_DATA_READY : 3
};
定義一個名稱空間 uikit,并聲明一個靜態(tài)的常量:EventTypes,此變量定義了目前系統(tǒng)所支持的事件類型。
uikit.event.JSEvent = Base.extend({
constructor : function(obj){
this.type = obj.type || uikit.event.EventTypes.EVENT_NONE;
this.object = obj.data || {};
},
getType : function(){
return this.type;
},
getObject : function(){
return this.object;
}
});
定義事件類,事件包括類型和事件中包含的數(shù)據(jù),通常為事件發(fā)生的點上的一些信息,比如點擊一個表格的某個單元格,可能需要將該單元格所在的行號和列號包裝進事件的數(shù)據(jù)。
uikit.event.JSEventListener = Base.extend({
constructor : function(listener){
this.sense = listener.sense;
this.handle = listener.handle || function(event){};
},
getSense : function(){
return this.sense;
}
});
定義事件監(jiān)聽器類,事件監(jiān)聽器包含兩個屬性,及監(jiān)聽器所關(guān)心的事件類型 sense 和當(dāng)該類型的事件發(fā)生后要做的動作 handle。
uikit.event.JSEventDispatcher = function(){
if(uikit.event.JSEventDispatcher.singlton){
return uikit.event.JSEventDispatcher.singlton;
}
this.listeners = {};
uikit.event.JSEventDispatcher.singlton = this;
this.post = function(event){
var handlers = this.listeners[event.getType()];
for(var index in handlers){
if(handlers[index].handle && typeof handlers[index].handle == "function")
handlers[index].handle(event);
}
};
this.addEventListener = function(listener){
var item = listener.getSense();
var listeners = this.listeners[item];
if(listeners){
this.listeners[item].push(listener);
}else{
var hList = new Array();
hList.push(listener);
this.listeners[item] = hList;
}
};
}
uikit.event.JSEventDispatcher.getInstance = function(){
return new uikit.event.JSEventDispatcher();
};
這里定義了一個單例的事件分發(fā)器,同一個系統(tǒng)中的任何組件都可以向此實例注冊自己,或者發(fā)送事件到此實例。事件分發(fā)器事實上需要為何這樣一個數(shù)據(jù)結(jié)構(gòu):
var listeners = {
eventType.foo : [
{sense : "eventType.foo", handle : function(){doSomething();}}
{sense : "eventType.foo", handle : function(){doSomething();}}
{sense : "eventType.foo", handle : function(){doSomething();}}
],
eventType.bar : [
{sense : "eventType.bar", handle : function(){doSomething();}}
{sense : "eventType.bar", handle : function(){doSomething();}}
{sense : "eventType.bar", handle : function(){doSomething();}}
],..
};
當(dāng)事件發(fā)生之后,分發(fā)器會找到該事件處理器的數(shù)組,然后依次調(diào)用監(jiān)聽器的 handle 方法進行相應(yīng)。好了,到此為止,我們已經(jīng)有了事件分發(fā)器的基本框架了,下來,我們開始實現(xiàn)我們的組件(Component)。
組件要通信,則需要加入事件支持,因此可以抽取出一個類:
uikit.component = uikit.component || {};
uikit.component.EventSupport = Base.extend({
constructor : function(){
},
raiseEvent : function(eventdef){
var e = new uikit.event.JSEvent(eventdef);
uikit.event.JSEventDispatcher.getInstance().post(e);
},
addActionListener : function(listenerdef){
var l = new uikit.event.JSEventListener(listenerdef);
uikit.event.JSEventDispatcher.getInstance().addEventListener(l);
}
});
繼承了這個類的類具有事件支持的能力,可以 raise 事件,也可以注冊監(jiān)聽器,這個 EventSupport 僅僅做了一個代理,將實際的工作代理到事件分發(fā)器上。
uikit.component.ComponentBase = uikit.component.EventSupport.extend({
constructor: function(canvas) {
this.canvas = canvas;
},
render : function(datamodel){}
});
定義所有的組件的基類,一般而言,組件需要有一個畫布(canvas)的屬性,而且組件需要有展現(xiàn)自己的能力,因此需要實現(xiàn) render 方法來畫出自己來。
我們來看一個繼承了 ComponentBase 的類 JSList:
uikit.component.JSList = uikit.component.ComponentBase.extend({
constructor : function(canvas, datamodel){
this.base(canvas);
this.render(datamodel);
},
render : function(datamodel){
var jqo = $(this.canvas);
var text = "";
for(var p in datamodel.items){
text += datamodel.items[p] + ";";
}
var item = $("<div></div>").addClass("component");
item.text(text);
item.click(function(){
jqo.find("div.selected").removeClass("selected");
$(this).addClass("selected");
var idx = jqo.find("div").index($(".selected")[0]);
var c = new uikit.component.ComponentBase(null);
c.raiseEvent({
type : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
data : {index : idx}
});
});
jqo.append(item);
},
update : function(event){
var jqo = $(this.canvas);
jqo.empty();
var dm = event.getObject().items;
for(var i = 0; i < dm.length();i++){
var entity = dm.get(i).item;
jqo.append(this.createItem({items : entity}));
}
},
createItem : function(datamodel){
var jqo = $(this.canvas);
var text = datamodel.items;
var item = $("<div></div>").addClass("component");
item.text(text);
item.click(function(){
jqo.find("div.selected").removeClass("selected");
$(this).addClass("selected");
var idx = jqo.find("div").index($(".selected")[0]);
var c = new uikit.component.ComponentBase(null);
c.raiseEvent({
type : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
data : {index : idx}
});
});
return item;
},
getSelectedItemIndex : function(){
var jqo = $(this.canvas);
var index = jqo.find("div").index($(".selected")[0]);
return index;
}
});
首先,我們的畫布其實是一個共 jQuery 選擇的選擇器,選擇到這個畫布之后,通過jQuery則可以比較容易的在畫布上繪制組件。
在我們的實現(xiàn)中,數(shù)據(jù)與視圖是分離的,我們通過定義這樣的數(shù)據(jù)結(jié)構(gòu):
{items : ["China", "Canada", "U.S.A", "U.K", "Uruguay"]};
則可以 render 出如下圖所示的 List:
http://wiki.jikexueyuan.com/project/javascript-core/images/js11.png" alt="" />
好,既然組件模型已經(jīng)有了,事件分發(fā)器的框架也有了,相信你已經(jīng)迫不及待的想要看看這些代碼可以干點什么了吧,再耐心一下,我們還要寫一點代碼:
$(document).ready(function(){
var ldmap = new uikit.component.ArrayLike(dataModel);
ldmap.addActionListener({
sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
handle : function(event){
var idx = event.getObject().index;
uikit.component.EventGenerator.raiseEvent({
type : uikit.event.EventTypes.EVENT_GRID_DATA_READY,
data : {rows : ldmap.get(idx).grid}
});
}
});
var list = new uikit.component.JSList("div#componentList", []);
var grid = new uikit.component.JSGrid("div#conditionsTable table tbody");
list.addActionListener({
sense : uikit.event.EventTypes.EVENT_LIST_DATA_READY,
handle : function(event){
list.update(event);
}
});
grid.addActionListener({
sense : uikit.event.EventTypes.EVENT_GRID_DATA_READY,
handle : function(event){
grid.update(event);
}
});
uikit.component.EventGenerator.raiseEvent({
type : uikit.event.EventTypes.EVENT_LIST_DATA_READY,
data : {items : ldmap}
});
var colorPanel = new uikit.component.Panel("div#colorPanel");
colorPanel.addActionListener({
sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
handle : function(event){
var idx = parseInt(10*Math.random())
colorPanel.update(idx);
}
});
});
使用 jQuery,我們在文檔加載完畢之后,新建了兩個對象 List 和 Grid,通過點擊 List 上的條目,如果這些條目在 List 的模型上索引發(fā)生變化,則會發(fā)出 EVENT_INDEX_CHAGE 事件,接收到這個事件的組件或者 DataModel 會做出相應(yīng)的響應(yīng)。在本例中,ldmap 在接收到 EVENT_INDEX_CHANGE 事件后,會組織數(shù)據(jù),并發(fā)出 EVENT_GRID_DATA_READY 事件,而 Grid 接收到這個事件后,根據(jù)事件對象上綁定的數(shù)據(jù)模型來更新自己的 UI。 上例中的類繼承關(guān)系如下圖:
http://wiki.jikexueyuan.com/project/javascript-core/images/js12.png" alt="" />
圖 事件分發(fā)器類層次
應(yīng)該注意的是,在綁定完監(jiān)聽器之后,我們手動的觸發(fā)了EVENT_LIST_DATA_READY事件,來通知List可以繪制自身了:
uikit.component.EventGenerator.raiseEvent({
type : uikit.event.EventTypes.EVENT_LIST_DATA_READY,
data : {items : ldmap}
});
在實際的應(yīng)用中,這個事件可能是用戶在頁面上點擊一個按鈕,或者一個 Ajax 請求的返回,等等,一旦事件監(jiān)聽器注冊完畢,程序就已經(jīng)就緒,等待異步事件并響應(yīng)。
點擊 List 中的元素 China,Grid 中的數(shù)據(jù)發(fā)生變化
http://wiki.jikexueyuan.com/project/javascript-core/images/js13.png" alt="" />
點擊 Canada,Grid 中的數(shù)據(jù)同樣發(fā)生相應(yīng)的變化:
http://wiki.jikexueyuan.com/project/javascript-core/images/js14.png" alt="" />
由于 List 和 Grid 的數(shù)據(jù)是關(guān)聯(lián)在一起的,他們的數(shù)據(jù)結(jié)構(gòu)具有下列的結(jié)構(gòu):
var dataModel = [{
item: "China",
grid: [
[{
dname: "Beijing",
type: "string"
},
{
dname: "ProductA",
type: "string"
},
{
dname: 1000,
type: "number"
}],
[{
dname: "ShangHai",
type: "string"
},
{
dname: "ProductB",
type: "string"
},
{
dname: 23451,
type: "number"
}],
[{
dname: "GuangZhou",
type: "string"
},
{
dname: "ProductB",
type: "string"
},
{
dname: 87652,
type: "number"
}]
]
},...
];
一個組件可以發(fā)出多種事件,同時也可以監(jiān)聽多種事件,所以我們可以為 List 的下標改變事件注冊另一個監(jiān)聽器,監(jiān)聽器為一個簡單組件 Panel,當(dāng)接收到這個事件后,該 Panel 會根據(jù)一個隨機的顏色來重置自身的背景色(注意在 List 和 Grid 下面的灰色 Panel):
http://wiki.jikexueyuan.com/project/javascript-core/images/js15.png" alt="" />