JS中是弱類型的,他的內置類型簡單而且清晰:
---------------------------------------------------------
undefined : 未定義
number : 數字
boolean : 布爾值
string : 字符串
function : 函數
object : 對象
1). undefined類型
========================
在IE5及以下版本中,除了直接賦值和typeof()之外,其它任何對undefined的操作都將導致
異常。如果需要知道一個變量是否是undefined,只能采用typeof()的方法:
輸出1234
testFoo(1234);
// 2. 嘗試輸出obj1
// 3. 嘗試輸出obj2
testFoo(obj1);
try {
testFoo(obj2);
}
catch(e) {
document.writeln('Exception: ', e.description, '
');
}
// 聲明testFoo()
function testFoo(v) {
document.writeln(v, '
');
}
// 聲明object
var obj1 = {};
obj2 = {
toString: function() {return 'hi, object.'}
}
// 4. 輸出obj1
// 5. 輸出obj2
testFoo(obj1);
testFoo(obj2);
這個示例代碼在JS環境中執行的結果是:
------------------------------------
1234
undefined
Exception: 'obj2' 未定義
[object Object]
hi, obj
------------------------------------
問題是,testFoo()是在它被聲明之前被執行的;而同樣用“直接聲明”的
形式定義的object變量,卻不能在聲明之前引用。——例子中,第二、三
個輸入是不正確的。
函數可以在聲明之前引用,而其它類型的數值必須在聲明之后才能被使用。
這說明“聲明”與“執行期引用”在JavaScript中是兩個過程。
另外我們也可以發現,使用"var"來聲明的時候,編譯器會先確認有該變量
存在,但變量的值會是“undefined”。——因此“testFoo(obj1)”不會發
生異常。但是,只有等到關于obj1的賦值語句被執行過,才會有正常的輸出。
請對照第二、三與第四、五行輸出的差異。
由于JavaScript對原型鏈的維護是“執行”而不是“聲明”,這說明“原型
鏈是由用戶代碼來維護的,而不是編譯器維護的。
由這個推論,我們來看下面這個例子:
//---------------------------------------------------------
// 示例:錯誤的原型鏈
//---------------------------------------------------------
// 1. 構造器
function Animal() {}; // 動物
function Mammal() {}; // 哺乳動物
function Canine() {}; // 犬科的哺乳動物
// 2. 構造原型鏈
var instance = new Mammal();
Mammal.prototype = new Animal();
Canine.prototype = instance;
// 3. 測試輸出
var obj = new Canine();
document.writeln(obj instanceof Animal);
這個輸出結果,使我們看到一個錯誤的原型鏈導致的結果“犬科的哺乳動
物‘不是'一種動物”。
根源在于“2. 構造原型鏈”下面的幾行代碼是解釋執行的,而不是象var和
function那樣是“聲明”并在編譯期被理解的。解決問題的方法是修改那三
行代碼,使得它的“執行過程”符合邏輯:
//---------------------------------------------------------
// 上例的修正代碼(部分)
//---------------------------------------------------------
// 2. 構造原型鏈
Mammal.prototype = new Animal();
var instance = new Mammal();
Canine.prototype = instance;
3). 原型實例是如何被構造過程使用的
------
仍以Delphi為例。構造過程中,delphi中會首先創建一個指定實例大小的
“空的對象”,然后逐一給屬性賦值,以及調用構造過程中的方法、觸發事
件等。
JavaScript中的new()關鍵字中隱含的構造過程,與Delphi的構造過程并不完全一致。但
在構造器函數中發生的行為卻與上述的類似:
//---------------------------------------------------------
// JS中的構造過程(形式代碼)
//---------------------------------------------------------
function MyObject2() {
this.prop = 3;
this.method = a_method_function;
if (you_want) {
this.method();
this.fire_OnCreate();
}
}
MyObject2.prototype = new MyObject(); // MyObject()的聲明略
var obj = new MyObject2();
如果以單個類為參考對象的,這個構造過程中JavaScript可以擁有與Delphi
一樣豐富的行為。然而,由于Delphi中的構造過程是“動態的”,因此事實上
Delphi還會調用父類(MyObject)的構造過程,以及觸發父類的OnCreate()事件。
JavaScript沒有這樣的特性。父類的構造過程僅僅發生在為原型(prototype
屬性)賦值的那一行代碼上。其后,無論有多少個new MyObject2()發生,
MyObject()這個構造器都不會被使用。——這也意味著:
- 構造過程中,原型對象是一次性生成的;新對象只持有這個原型實例的引用
(并用“寫復制”的機制來存取其屬性),而并不再調用原型的構造器。
由于不再調用父類的構造器,因此Delphi中的一些特性無法在JavaScript中實現。
這主要影響到構造階段的一些事件和行為。——無法把一些“對象構造過程中”
的代碼寫到父類的構造器中。因為無論子類構造多少次,這次對象的構造過程根
本不會激活父類構造器中的代碼。
JavaScript中屬性的存取是動態的,因為對象存取父類屬性依賴于原型鏈表,構造
過程卻是靜態的,并不訪問父類的構造器;而在Delphi等一些編譯型語言中,(不使
用讀寫器的)屬性的存取是靜態的,而對象的構造過程則動態地調用父類的構造函數。
所以再一次請大家看清楚new()關鍵字的形式代碼中的這一行:
//---------------------------------------------------------
// new()關鍵字的形式化代碼
//---------------------------------------------------------
function new(aFunction) {
// 原型引用
var _proto= aFunction.prototype;
// ...
}
這個過程中,JavaScript做的是“get a prototype_Ref”,而Delphi等其它語言做
的是“Inherited Create()”。
八、JavaScript面向對象的支持
~~~~~~~~~~~~~~~~~~
(續)
4). 需要用戶維護的另一個屬性:constructor
------
回顧前面的內容,我們提到過:
- (如果正常地實現繼承模型,)對象實例的constructor屬性指向構造器
- obj.constructor.prototype指向該對象的原型
- 通過Object.constructor屬性,可以檢測obj2與obj1是否是相同類型的實例
與原型鏈要通過用戶代碼來維護prototype屬性一樣,實例的構造器屬性constructor
也需要用戶代碼維護。
對于JavaScript的內置對象來說,constructor屬性指向內置的構造器函數。如:
//---------------------------------------------------------
// 內置對象實例的constructor屬性
//---------------------------------------------------------
var _object_types = {
'function' : Function,
'boolean' : Boolean,
'regexp' : RegExp,
// 'math' : Math,
// 'debug' : Debug,
// 'image' : Image;
// 'undef' : undefined,
// 'dom' : undefined,
// 'activex' : undefined,
'vbarray' : VBArray,
'array' : Array,
'string' : String,
'date' : Date,
'error' : Error,
'enumerator': Enumerator,
'number' : Number,
'object' : Object
}
function objectTypes(obj) {
if (typeof obj !== 'object') return typeof obj;
if (obj === null) return 'null';
for (var i in _object_types) {
if (obj.constructor===_object_types[i]) return i;
}
return 'unknow';
}
// 測試數據和相關代碼
function MyObject() {
}
function MyObject2() {
}
MyObject2.prototype = new MyObject();
window.execScript(''+
'Function CreateVBArray()' +
' Dim a(2, 2)' +
' CreateVBArray = a' +
'End Function', 'VBScript');
document.writeln('dom<', '/div>');
// 測試代碼
var ax = new ActiveXObject("Microsoft.XMLHTTP");
var dom = document.getElementById('dom');
var vba = new VBArray(CreateVBArray());
var obj = new MyObject();
var obj2 = new MyObject2();
document.writeln(objectTypes(vba), '
');
document.writeln(objectTypes(ax), '
');
document.writeln(objectTypes(obj), '
');
document.writeln(objectTypes(obj2), '
');
document.writeln(objectTypes(dom), '
');
在這個例子中,我們發現constructor屬性被實現得并不完整。對于DOM對象、ActiveX對象
來說這個屬性都沒有正確的返回。
確切的說,DOM(包括Image)對象與ActiveX對象都不是標準JavaScript的對象體系中的,
因此它們也可能會具有自己的constructor屬性,并有著與JavaScript不同的解釋。因此,
JavaScript中不維護它們的constructor屬性,是具有一定的合理性的。
另外的一些單體對象(而非構造器),也不具有constructor屬性,例如“Math”和“Debug”、
“Global”和“RegExp對象”。他們是JavaScript內部構造的,不應該公開構造的細節。
我們也發現實例obj的constructor指向function MyObject()。這說明JavaScript維護了對
象的constructor屬性。——這與一些人想象的不一樣。
然而再接下來,我們發現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示例)
//---------------------------------------------------------
結果為值類型,或變量為值類型時,等值(或全等)比較可以得到預想結果
- (即使包含相同的數據,)不同的對象實例之間是不等值(或全等)的
- 同一個對象的不同引用之間,是等值(==)且全等(===)的
但對于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(), '
');
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), '
');
// 測試二
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中的問題。
但這本書在“上下文環境鏈表”的查詢方式上的講述,是正確的而合理的。只是把這個叫成“作用域”有點不對,或者不妥。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com