然而再接下來,我們發現MyObject2()的實例obj2的constructor仍然指向function MyObject()。 盡管這很說不通,然而現實的確如此。——這到底是為什么呢?
事實上,僅下面的代碼: -------- function MyObject2() { }
obj2 = new MyObject2(); document.writeln(MyObject2.prototype.constructor === MyObject2); -------- 構造的obj2.constructor將正確的指向function MyObject2()。事實上,我們也會注意到這 種情況下,MyObject2的原型屬性的constructor也正確的指向該函數。然而,由于JavaScript 要求指定prototype對象來構造原型鏈: -------- function MyObject2() { } MyObject2.prototype = new MyObject();
obj2 = new MyObject2(); -------- 這時,再訪問obj2,將會得到新的原型(也就是MyObject2.prototype)的constructor屬性。 因此,一切很明了:原型的屬性影響到構造過程對對象的constructor的初始設定。
作為一種補充的解決問題的手段,JavaScript開發規范中說“need to remember to reset the constructor property',要求用戶自行設定該屬性。
所以你會看到更規范的JavaScript代碼要求這樣書寫: //--------------------------------------------------------- // 維護constructor屬性的規范代碼 //--------------------------------------------------------- function MyObject2() { } MyObject2.prototype = new MyObject(); MyObject2.prototype.constructor = MyObject2;
obj2 = new MyObject2();
更外一種解決問題的方法,是在function MyObject()中去重置該值。當然,這樣會使 得執行效率稍低一點點: //--------------------------------------------------------- // 維護constructor屬性的第二種方式 //--------------------------------------------------------- function MyObject2() { ?? this.constructor = arguments.callee; ?? // or, this.constructor = MyObject2;
?? // ... } MyObject2.prototype = new MyObject();
obj2 = new MyObject2();
5). 析構問題 ------ JavaScript中沒有析構函數,但卻有“對象析構”的問題。也就是說,盡管我們不 知道一個對象什么時候會被析構,也不能截獲它的析構過程并處理一些事務。然而, 在一些不多見的時候,我們會遇到“要求一個對象立即析構”的問題。
問題大多數的時候出現在對ActiveX Object的處理上。因為我們可能在JavaScript 里創建了一個ActiveX Object,在做完一些處理之后,我們又需要再創建一個。而 如果原來的對象供應者(Server)不允許創建多個實例,那么我們就需要在JavaScript 中確保先前的實例是已經被釋放過了。接下來,即使Server允許創建多個實例,而 在多個實例間允許共享數據(例如OS的授權,或者資源、文件的鎖),那么我們在新 實例中的操作就可能會出問題。
可能還是有人不明白我們在說什么,那么我就舉一個例子:如果創建一個Excel對象, 打開文件A,然后我們save它,然后關閉這個實例。然后我們再創建Excel對象并打開 同一文件。——注意這時JavaScript可能還沒有來得及析構前一個對象。——這時我們 再想Save這個文件,就發現失敗了。下面的代碼示例這種情況: //--------------------------------------------------------- // JavaScript中的析構問題(ActiveX Object示例) //--------------------------------------------------------- <script> var strSaveLocation = 'file:///E:/1.xls'
function createXLS() { ?? var excel = new ActiveXObject("Excel.Application"); ?? var wk = excel.Workbooks.Add(); ?? wk.SaveAs(strSaveLocation); ?? wk.Saved = true;
?? excel.Quit(); }
function writeXLS() { ?? var excel = new ActiveXObject("Excel.Application"); ?? var wk = excel.Workbooks.Open(strSaveLocation); ?? var sheet = wk.Worksheets(1); ?? sheet.Cells(1, 1).Value = '測試字符串'; ?? wk.SaveAs(strSaveLocation); ?? wk.Saved = true;
?? excel.Quit(); } </script> <body> ?? <button onclick="createXLS()">創建</button> ?? <button onclick="writeXLS()">重寫</button> </body>
在這個例子中,在本地文件操作時并不會出現異常。——最多只是有一些內存垃 圾而已。然而,如果strSaveLocation是一個遠程的URL,這時本地將會保存一個 文件存取權限的憑證,而且同時只能一個(遠程的)實例來開啟該excel文檔并存 儲。于是如果反復點擊"重寫"按鈕,就會出現異常。
——注意,這是在SPS中操作共享文件時的一個實例的簡化代碼。因此,它并非 “學術的”無聊討論,而且工程中的實際問題。
解決這個問題的方法很復雜。它涉及到兩個問題: ?? - 本地憑證的釋放 ?? - ActiveX Object實例的釋放
下面我們先從JavaScript中對象的“失效”問題說起。簡單的說: ?? - 一個對象在其生存的上下文環境之外,即會失效。 ?? - 一個全局的對象在沒有被執用(引用)的情況下,即會失效。
例如: //--------------------------------------------------------- // JavaScript對象何時失效 //--------------------------------------------------------- function testObject() { ?? var _obj1 = new Object(); }
function testObject2() { ?? var _obj2 = new Object(); ?? return _obj2; }
// 示例1 testObject();
// 示例2 testObject2()
// 示例3 var obj3 = testObject2(); obj3 = null;
// 示例4 var obj4 = testObject2(); var arr = [obj4]; obj3 = null; arr = [];
在這四個示例中: ?? - “示例1”在函數testObject()中構造了_obj1,但是在函數退出時, ???? 它就已經離開了函數的上下文環境,因此_obj1失效了; ?? - “示例2”中,testObject2()中也構造了一個對象_obj2并傳出,因 ???? 此對象有了“函數外”的上下文環境(和生存周期),然而由于函數 ???? 的返回值沒有被其它變量“持有”,因此_obj2也立即失效了; ?? - “示例3”中,testObject2()構造的_obj2被外部的變量obj3持用了, ???? 這時,直到“obj3=null”這行代碼生效時,_obj2才會因為引用關系 ???? 消失而失效。 ?? - 與示例3相同的原因,“示例4”中的_obj2會在“arr=[]”這行代碼 ???? 之后才會失效。
但是,對象的“失效”并不等會“釋放”。在JavaScript運行環境的內部,沒 有任何方式來確切地告訴用戶“對象什么時候會釋放”。這依賴于JavaScript 的內存回收機制。——這種策略與.NET中的回收機制是類同的。
在前面的Excel操作示例代碼中,對象的所有者,也就是"EXCEL.EXE"這個進程 只能在“ActiveX Object實例的釋放”之后才會發生。而文件的鎖,以及操作 系統的權限憑證是與進程相關的。因此如果對象僅是“失效”而不是“釋放”, 那么其它進程處理文件和引用操作系統的權限憑據時就會出問題。
——有些人說這是JavaScript或者COM機制的BUG。其實不是,這是OS、IE 和JavaScript之間的一種復雜關系所導致的,而非獨立的問題。
Microsoft公開了解決這種問題的策略:主動調用內存回收過程。
在(微軟的)JScript中提供了一個CollectGarbage()過程(通常簡稱GC過程), GC過程用于清理當前IE中的“失效的對象失例”,也就是調用對象的析構過程。
在上例中調用GC過程的代碼是: //--------------------------------------------------------- // 處理ActiveX Object時,GC過程的標準調用方式 //--------------------------------------------------------- function writeXLS() { ?? //(略...)
?? excel.Quit(); ?? excel = null; ?? setTimeout(CollectGarbage, 1); }
第一行代碼調用excel.Quit()方法來使得excel進程中止并退出,這時由于JavaScript 環境執有excel對象實例,因此excel進程并不實際中止。
第二行代碼使excel為null,以清除對象引用,從而使對象“失效”。然而由于 對象仍舊在函數上下文環境中,因此如果直接調用GC過程,對象仍然不會被清理。
第三行代碼使用setTimeout()來調用CollectGarbage函數,時間間隔設為'1',只 是使得GC過程發生在writeXLS()函數執行完之后。這樣excel對象就滿足了“能被 GC清理”的兩個條件:沒有引用和離開上下文環境。
GC過程的使用,在使用了ActiveX Object的JS環境中很有效。一些潛在的ActiveX Object包括XML、VML、OWC(Office Web Componet)、flash,甚至包括在JS中的VBArray。 從這一點來看,ajax架構由于采用了XMLHTTP,并且同時要滿足“不切換頁面”的 特性,因此在適當的時候主動調用GC過程,會得到更好的效率用UI體驗。
事實上,即使使用GC過程,前面提到的excel問題仍然不會被完全解決。因為IE還 緩存了權限憑據。使頁的權限憑據被更新的唯一方法,只能是“切換到新的頁面”, 因此事實上在前面提到的那個SPS項目中,我采用的方法并不是GC,而是下面這一 段代碼: //--------------------------------------------------------- // 處理ActiveX Object時采用的頁面切換代碼 //--------------------------------------------------------- function writeXLS() { ?? //(略...)
?? excel.Quit(); ?? excel = null;
?? // 下面代碼用于解決IE call Excel的一個BUG, MSDN中提供的方法: ?? //??? setTimeout(CollectGarbage, 1); ?? // 由于不能清除(或同步)網頁的受信任狀態, 所以將導致SaveAs()等方法在 ?? // 下次調用時無效. ?? location.reload(); }
最后之最后,關于GC的一個補充說明:在IE窗體被最小化時,IE將會主動調用一次 CollectGarbage()函數。這使得IE窗口在最小化之后,內存占用會有明顯改善。
八、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節也講述了這個問題,但被叫做“變量的作用域”。然而重要的是,這本書把問題講反了。——作者試圖用“全局、局部的作用域”,來解釋產生這種現象的“上下文環境”的問題。因此這個小節顯得凌亂而且難以自圓其說。
|