javascript|對象
======================================================================
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中的問題。
但這本書在“上下文環境鏈表”的查詢方式上的講述,是正確的而合理的。只是把這個叫成“範圍”有點不對,或者不妥。