================================================================================
Qomolangma OpenProject v0.9
類別 :Rich Web Client
關鍵詞 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
DOM,DTHML,CSS,JavaScript,JScript
項目發起:aimingoo (aim@263.net)
項目團隊:aimingoo, leon(pfzhou@gmail.com)
有貢獻者:JingYu(zjy@cnpack.org)
================================================================================
八、JavaScript物件導向的支援
~~~~~~~~~~~~~~~~~~
(續)
4. 執行個體和執行個體引用
--------
在.NET Framework對CTS(Common Type System)約定“一切都是對象”,並分為“值
類型”和“參考型別”兩種。其中“實值型別”的對象在轉換成“參考型別”資料的
過程中,需要進行一個“裝箱”和“拆箱”的過程。
在JavaScript也有同樣的問題。我們看到的typeof關鍵字,返回以下六種資料類型:
"number"、"string"、"boolean"、"object"、"function" 和 "undefined"。
我們也發現JavaScript的對象系統中,有String、Number、Function、Boolean這四
種物件建構器。那麼,我們的問題是:如果有一個數字A,typeof(A)的結果,到底會
是'number'呢,還是一個構造器指向function Number()的對象呢?
//---------------------------------------------------------
// 關於JavaScript的類型的測試代碼
//---------------------------------------------------------
function getTypeInfo(V) {
return (typeof V == 'object' ? 'Object, construct by '+V.constructor
: 'Value, type of '+typeof V);
}
var A1 = 100;
var A2 = new Number(100);
document.writeln('A1 is ', getTypeInfo(A1), '<BR>');
document.writeln('A2 is ', getTypeInfo(A2), '<BR>');
document.writeln([A1.constructor === A2.constructor, A2.constructor === Number]);
測試代碼的執行結果如下:
-----------
A1 is Value, type of number
A2 is Object, construct by function Number() { [native code] }
true,true
-----------
我們注意到,A1和A2的構造器都指向Number。這意味著通過constructor屬性來識別
對象,(有時)比typeof更加有效。因為“實值型別資料”A1作為一個對象來看待時,
與A2有完全相同的特性。
——除了與執行個體引用有關的問題。
參考JScript手冊,我們對其它基礎類型和構造器做相同考察,可以發現:
- 基礎類型中的undefined、number、boolean和string,是“實值型別”變數
- 基礎類型中的array、function和object,是“參考型別”變數
- 使用new()方法構造出對象,是“參考型別”變數
下面的代碼說明“實值型別”與“參考型別”之間的區別:
//---------------------------------------------------------
// 關於JavaScript類型系統中的值/引用問題
//---------------------------------------------------------
var str1 = 'abcdefgh', str2 = 'abcdefgh';
var obj1 = new String('abcdefgh'), obj2 = new String('abcdefgh');
document.writeln([str1==str2, str1===str2], '<br>');
document.writeln([obj1==obj2, obj1===obj2]);
測試代碼的執行結果如下:
-----------
true, true
false, false
-----------
我們看到,無論是等值運算(==),還是全等運算(===),對“對象”和“值”的
理解都是不一樣的。
更進一步的理解這種現象,我們知道:
- 運算結果為實值型別,或變數為實值型別時,等值(或全等)比較可以得到預想結果
- (即使包含相同的資料,)不同的對象執行個體之間是不等值(或全等)的
- 同一個對象的不同引用之間,是等值(==)且全等(===)的
但對於String類型,有一點補充:根據JScript的描述,兩個字串比較時,只要有
一個是實值型別,則按值比較。這意味著在上面的例子中,代碼“str1==obj1”會得到
結果true。而全等(===)運算需要檢測變數類型的一致性,因此“str1===obj1”的結
果返回false。
JavaScript中的函數參數總是傳入值參,參考型別(的執行個體)是作為指標值傳入的。因此
函數可以隨意重寫入口變數,而不用擔心外部變數被修改。但是,需要留意傳入的引用
類型的變數,因為對它方法調用和屬性讀寫可能會影響到執行個體本身。——但,也可以通
過參考型別的參數來傳出資料。
最後補充說明一下,實值型別比較會逐位元組檢測對象執行個體中的資料,效率低但準確性高;
而參考型別只檢測執行個體指標和資料類型,因此效率高而準確性低。如果你需要檢測兩個
參考型別是否真的包含相同的資料,可能你需要嘗試把它轉換成“字串值”再來比較。
6. 函數的上下文環境
--------
只要寫過代碼,你應該知道變數是有“全域變數”和“局部變數”之分的。絕大多數的
JavaScript程式員也知道下面這些概念:
//---------------------------------------------------------
// JavaScript中的全域變數與局部變數
//---------------------------------------------------------
var v1 = '全域變數-1';
v2 = '全域變數-2';
function foo() {
v3 = '全域變數-3';
var v4 = '只有在函數內部並使用var定義的,才是局部變數';
}
按照通常對語言的理解來說,不同的代碼調用函數,都會擁有一套獨立的局部變數。
因此下面這段代碼很容易理解:
//---------------------------------------------------------
// JavaScript的局部變數
//---------------------------------------------------------
function MyObject() {
var o = new Object;
this.getValue = function() {
return o;
}
}
var obj1 = new MyObject();
var obj2 = new MyObject();
document.writeln(obj1.getValue() == obj2.getValue());
結果顯示false,表明不同(執行個體的方法)調用返回的局部變數“obj1/obj2”是不相同。
變數的局部、全域特性與OOP的封裝性中的“私人(private)”、“公開(public)”具
有類同性。因此絕大多數資料總是以下面的方式來說明JavaScript的物件導向系統中
的“封裝權限等級”問題:
//---------------------------------------------------------
// JavaScript中OOP封裝性
//---------------------------------------------------------
function MyObject() {
// 1. 私人成員和方法
var private_prop = 0;
var private_method_1 = function() {
// ...
return 1
}
function private_method_2() {
// ...
return 1
}
// 2. 特權方法
this.privileged_method = function () {
private_prop++;
return private_prop + private_method_1() + private_method_2();
}
// 3. 公開成員和方法
this.public_prop_1 = '';
this.public_method_1 = function () {
// ...
}
}
// 4. 公開成員和方法(2)
MyObject.prototype.public_prop_1 = '';
MyObject.prototype.public_method_1 = function () {
// ...
}
var obj1 = new MyObject();
var obj2 = new MyObject();
document.writeln(obj1.privileged_method(), '<br>');
document.writeln(obj2.privileged_method());
在這裡,“私人(private)”表明只有在(構造)函數內部可訪問,而“特權(privileged)”
是特指一種存取“私人域”的“公開(public)”方法。“公開(public)”表明在(構造)函
數外可以調用和存取。
除了上述的封裝許可權之外,一些文檔還介紹了其它兩種相關的概念:
- 原型屬性:Classname.prototype.propertyName = someValue
- (類)靜態屬性:Classname.propertyName = someValue
然而,從物件導向的角度上來講,上面這些概念都很難自圓其說:JavaScript究竟是為何、
以及如何劃分出這些封裝許可權和概念來的呢?
——因為我們必須注意到下面這個例子所帶來的問題:
//---------------------------------------------------------
// JavaScript中的局部變數
//---------------------------------------------------------
function MyFoo() {
var i;
MyFoo.setValue = function (v) {
i = v;
}
MyFoo.getValue = function () {
return i;
}
}
MyFoo();
var obj1 = new Object();
var obj2 = new Object();
// 測試一
MyFoo.setValue.call(obj1, 'obj1');
document.writeln(MyFoo.getValue.call(obj1), '<BR>');
// 測試二
MyFoo.setValue.call(obj2, 'obj2');
document.writeln(MyFoo.getValue.call(obj2));
document.writeln(MyFoo.getValue.call(obj1));
document.writeln(MyFoo.getValue());
在這個測試代碼中,obj1/obj2都是Object()執行個體。我們使用function.call()的方式
來調用setValue/getValue,使得在MyFoo()調用的過程中替換this為obj1/obj2執行個體。
然而我們發現“測試二”完成之後,obj2、obj1以及function MyFoo()所持有的局部
變數都返回了“obj2”。——這表明三個函數使用了同一個局部變數。
由此可見,JavaScript在處理局部變數時,對“普通函數”與“構造器”是分別對待
的。這種處理策略在一些JavaScript相關的資料中被解釋作“物件導向中的私人域”
問題。而事實上,我更願意從原始碼一級來告訴你真相:這是對象的上下文環境的問
題。——只不過從表面看去,“上下文環境”的問題被轉嫁到對象的封裝性問題上了。
(在閱讀下面的文字之前,)先做一個概念性的說明:
- 在普通函數中,上下文環境被window對象所持有
- 在“構造器和對象方法”中,上下文環境被對象執行個體所持有
在JavaScript的實現代碼中,每次建立一個對象,解譯器將為對象建立一個上下文環境
鏈,用於存放對象在進入“構造器和對象方法”時對function()內部資料的一個備份。
JavaScript保證這個對象在以後再進入“構造器和對象方法”內部時,總是持有該上下
文環境,和一個與之相關的this對象。由於對象可能有多個方法,且每個方法可能又存
在多層嵌套函數,因此這事實上構成了一個上下文環境的樹型鏈表結構。而在構造器和
對象方法之外,JavaScript不提供任何訪問(該構造器和對象方法的)上下文環境的方法。
簡而言之:
- 上下文環境與對象執行個體調用“構造器和對象方法”時相關,而與(普通)函數無關
- 上下文環境記錄一個對象在“建構函式和對象方法”內部的私人資料
- 上下文環境採用鏈式結構,以記錄多層的嵌套函數中的上下文
由於上下文環境只與建構函式及其內部的嵌套函數有關,重新閱讀前面的代碼:
//---------------------------------------------------------
// JavaScript中的局部變數
//---------------------------------------------------------
function MyFoo() {
var i;
MyFoo.setValue = function (v) {
i = v;
}
MyFoo.getValue = function () {
return i;
}
}
MyFoo();
var obj1 = new Object();
MyFoo.setValue.call(obj1, 'obj1');
我們發現setValue()的確可以訪問到位於MyFoo()函數內部的“局部變數i”,但是由於
setValue()方法的執有者是MyFoo對象(記住函數也是對象),因此MyFoo對象擁有MyFoo()
函數的唯一一份“上下文環境”。
接下來MyFoo.setValue.call()調用雖然為setValue()傳入了新的this對象,但實際上
擁有“上下文環境”的仍舊是MyFoo對象。因此我們看到無論建立多少個obj1/obj2,最
終操作的都是同一個私人變數i。
全域函數/變數的“上下文環境”持有人為window,因此下面的代碼說明了“為什麼全
局變數能被任意的對象和函數訪問”:
//---------------------------------------------------------
// 全域函數的上下文
//---------------------------------------------------------
/*
function Window() {
*/
var global_i = 0;
var global_j = 1;
function foo_0() {
}
function foo_1() {
}
/*
}
window = new Window();
*/
因此我們可以看到foo_0()與foo_1()能同時訪問global_i和global_j。接下來的推論是,
上下文環境決定了變數的“全域”與“私人”。而不是反過來通過變數的私人與全域來
討論上下文環境問題。
更進一步的推論是:JavaScript中的全域變數與函數,本質上是window對象的私人變數
與方法。而這個上下文環境塊,位於所有(window對象內部的)對象執行個體的上下文環境鏈
表的頂端,因此都可能訪問到。
用“上下文環境”的理論,你可以順利地解釋在本小節中,有關變數的“全域/局部”
範圍的問題,以及有關對象方法的封裝許可權問題。事實上,在實現JavaScript的C源
代碼中,這個“上下文環境”被叫做“JSContext”,並作為函數/方法的第一個參數
傳入。——如果你有興趣,你可以從原始碼中證實本小節所述的理論。
另外,《JavaScript權威指南》這本書中第4.7節也講述了這個問題,但被叫做“變數
的範圍”。然而重要的是,這本書把問題講反了。——作者試圖用“全域、局部的作
用域”,來解釋產生這種現象的“上下文環境”的問題。因此這個小節顯得淩亂而且難
以自圓其說。
不過在4.6.3小節,作者也提到了執行環境(execution context)的問題,這就與我們這
裡說的“上下文環境”是一致的了。然而更麻煩的是,作者又將讀者引錯了方法,試圖
用函數的上下文環境去解釋DOM和ScriptEngine中的問題。
但這本書在“上下文環境鏈表”的查詢方式上的講述,是正確的而合理的。只是把這個
叫成“範圍”有點不對,或者不妥。