~~~~~~~~~~~~~~~~~~ 很少有人對JavaScript的面向?qū)ο筇匦赃M(jìn)行系統(tǒng)的分析,。我希望接下來的文字讓你了解到這 個語言最少為人知的一面,。
1. JavaScript中的類型 -------- 雖然JavaScript是一個基于對象的語言,但對象(Object)在JavaScript中不是第一型的,。JS 是以函數(shù)(Function)為第一型的語言。這樣說,,不但是因為JS中的函數(shù)具有高級語言中的函 數(shù)的各種特性,,而且也因為在JS中,Object也是由函數(shù)來實現(xiàn)的,?!P(guān)于這一點,可以在 后文中“構(gòu)造與析構(gòu)”部分看到更進(jìn)一步的說明,。
JS中是弱類型的,,他的內(nèi)置類型簡單而且清晰: --------------------------------------------------------- undefined : 未定義 number : 數(shù)字 boolean : 布爾值 string : 字符串 function : 函數(shù) object : 對象
1). undefined類型 ======================== 在IE5及以下版本中,除了直接賦值和typeof()之外,,其它任何對undefined的操作都將導(dǎo)致 異常,。如果需要知道一個變量是否是undefined,只能采用typeof()的方法: <script> var v; if (typeof(v) == ‘undefined‘) { // ... } </script>
但是在IE5.5及以上版本中,,undefined是一個已實現(xiàn)的系統(tǒng)保留字,。因此可以用undefined來 比較和運算。檢測一個值是否是undefined的更簡單方法可以是: <script> var v; if (v === undefined) { // ... } </script>
因此為了使得核心代碼能(部分地)兼容IE5及早期版本,,Romo核心單元中有一行代碼用來 “聲明”一個undefined值: //--------------------------------------------------------- // code from Qomolangma, in JSEnhance.js //--------------------------------------------------------- var undefined = void null;
這一行代碼還有一點是需要說明的,,就是void語句的應(yīng)用,。void表明“執(zhí)行其后的語句,且 忽略返回值”,。因此在void之后可以出現(xiàn)能被執(zhí)行的任何“單個”語句,。而執(zhí)行的結(jié)果就是 undefined。當(dāng)然,,如果你愿意,,你也可以用下面的代碼之一“定義undefined”。 //--------------------------------------------------------- // 1. 較復(fù)雜的方法,,利用一個匿名的空函數(shù)執(zhí)行的返回 //--------------------------------------------------------- var undefined = function(){}();
//--------------------------------------------------------- // 2. 代碼更簡潔,,但不易懂的方法 //--------------------------------------------------------- var undefined = void 0;
void也能像函數(shù)一樣使用,因此void(0)也是合法的,。有些時候,,一些復(fù)雜的語句可能不能 使用void的關(guān)鍵字形式,而必須要使用void的函數(shù)形式,。例如: //--------------------------------------------------------- // 必須使用void()形式的復(fù)雜表達(dá)式 //--------------------------------------------------------- void(i=1); // 或如下語句: void(i=1, i++);
2). number類型 ======================== JavaScript中總是處理浮點數(shù),,因此它沒有象Delphi中的MaxInt這樣的常量,反而是有這 樣兩個常值定義: Number.MAX_VALUE : 返回 JScript 能表達(dá)的最大的數(shù),。約等于 1.79E+308,。 Number.MIN_VALUE : 返回 JScript 最接近0的數(shù)。約等于 2.22E-308,。
因為沒有整型的緣故,,因此在一些關(guān)于CSS和DOM屬性的運算中,如果你期望取值為整數(shù)2,, 你可能會得到字符串“2.0”——或者類似于此的一些情況,。這種情況下,你可能需要用 到全局對象(Gobal)的parseInt()方法,。
全局對象(Gobal)中還有兩個屬性與number類型的運算有關(guān): NaN : 算術(shù)表達(dá)式的運算結(jié)果不是數(shù)字,,則返回NaN值。 Infinity : 比MAX_VALUE更大的數(shù),。
如果一個值是NaN,,那么他可以通過全局對象(Gobal)的isNaN()方法來檢測。然而兩個NaN 值之間不是互等的,。如下例: //--------------------------------------------------------- // NaN的運算與檢測 //--------------------------------------------------------- var v1 = 10 * ‘a‘; v2 = 10 * ‘a‘; document.writeln(isNaN(v1)); document.writeln(isNaN(v2)); document.writeln(v1 == v2);
全局對象(Gobal)的Infinity表示比最大的數(shù) (Number.MAX_VALUE) 更大的值,。在JS中, 它在數(shù)學(xué)運算時的價值與正無窮是一樣的,?!谝恍嵱眉记芍校部梢杂脕碜鲆?br>個數(shù)組序列的邊界檢測,。
Infinity在Number對象中被定義為POSITIVE_INFINITY,。此外,,負(fù)無窮也在Number中被定 義: Number.POSITIVE_INFINITY : 比最大正數(shù)(Number.MAX_VALUE)更大的值。正無窮,。 Number.NEGATIVE_INFINITY : 比最小負(fù)數(shù)(-Number.MAX_VALUE)更小的值,。負(fù)無窮。
與NaN不同的是,,兩個Infinity(或-Infinity)之間是互等的,。如下例: //--------------------------------------------------------- // Infinity的運算與檢測 //--------------------------------------------------------- var v1 = Number.MAX_VALUE * 2; v2 = Number.MAX_VALUE * 3; document.writeln(v1); document.writeln(v2); document.writeln(v1 == v2);
在Global中其它與number類型相關(guān)的方法有: isFinite() : 如果值是NaN/正無窮/負(fù)無窮,返回false,,否則返回true,。 parseFloat() : 從字符串(的前綴部分)取一個浮點數(shù)。不成功則返回NaN,。
3). boolean類型 ======================== (略)
4). string類型 ======================== JavaScript中的String類型原本沒有什么特殊的,,但是JavaScript為了適應(yīng) “瀏覽器實現(xiàn)的超文本環(huán)境”,因此它具有一些奇怪的方法,。例如: link() : 把一個有HREF屬性的超鏈接標(biāo)簽<A>放在String對象中的文本兩端,。 big() : 把一對<big>標(biāo)簽放在String對象中的文本兩端。 以下方法與此類同: anchor() blink() bold() fixed() fontcolor() fontsize() italics() small() strike() sub() sup()
除此之外,,string的主要復(fù)雜性來自于在JavaScript中無所不在的toString() 方法,。這也是JavaScript為瀏覽器環(huán)境而提供的一個很重要的方法。例如我們 聲明一個對象,,但是要用document.writeln()來輸出它,,在IE中會顯示什么呢?
下例說明這個問題: //--------------------------------------------------------- // toString()的應(yīng)用 //--------------------------------------------------------- var s = new Object();
s.v1 = ‘hi,‘; s.v2 = ‘test!‘; document.writeln(s); document.writeln(s.toString());
s.toString = function() { return s.v1 + s.v2; } document.writeln(s);
在這個例子中,,我們看到,,當(dāng)一個對象沒有重新聲明(覆蓋)自己toString()方 法的時候,那么它作為字符串型態(tài)使用時(例如被writeln),,就會調(diào)用Java Script 環(huán)境缺省的toString(),。反過來,,你也可以重新定義JavaScript理解這個對象 的方法,。
很多JavaScript框架,在實現(xiàn)“模板”機(jī)制的時候,,就利用了這個特性,。例如 他們用這樣定義一個FontElement對象: //--------------------------------------------------------- // 利用toString()實現(xiàn)模板機(jī)制的簡單原理 //--------------------------------------------------------- function FontElement(innerHTML) { this.face = ‘宋體‘; this.color = ‘red‘; // more...
var ctx = innerHTML; this.toString = function() { return ‘<Font FACE="‘ + this.face + ‘" COLOR="‘ + this.color + ‘">‘ + ctx + ‘</FONT>‘; } }
var obj = new FontElement(‘這是一個測試。‘);
// 留意下面這行代碼的寫法 document.writeln(obj);
5). function類型 ======================== javascript函數(shù)具有很多特性,,除了面向?qū)ο蟮牟糠种?這在后面講述),,它自 已的一些獨特特性應(yīng)用也很廣泛。
首先javascript中的每個函數(shù),,在調(diào)用過程中可以執(zhí)有一個arguments對象,。這個 對象是由腳本解釋環(huán)境創(chuàng)建的,,你沒有別的方法來自己創(chuàng)建一個arguments對象。
arguments可以看成一個數(shù)組:它有l(wèi)ength屬性,,并可以通過arguments[n]的方式 來訪問每一個參數(shù),。然而它最重要的,卻是可以通過 callee 屬性來得到正在執(zhí)行 的函數(shù)對象的引用,。
接下的問題變得很有趣:Function對象有一個 caller 屬性,,指向正在調(diào)用當(dāng)前 函數(shù)的父函數(shù)對象的引用。
——我們已經(jīng)看到,,我們可以在JavaScript里面,,通過callee/caller來遍歷執(zhí)行 期的調(diào)用棧。由于arguments事實上也是Function的一個屬性,,因此我們事實上也 能遍歷執(zhí)行期調(diào)用棧上的每一個函數(shù)的參數(shù),。下面的代碼是一個簡單的示例:
//--------------------------------------------------------- // 調(diào)用棧的遍歷 //--------------------------------------------------------- function foo1(v1, v2) { foo2(v1 * 100); }
function foo2(v1) { foo3(v1 * 200); }
function foo3(v1) { var foo = arguments.callee; while (foo && (foo != window)) { document.writeln(‘調(diào)用參數(shù):<br>‘, ‘---------------<br>‘);
var args = foo.arguments, argn = args.length; for (var i=0; i<argn; i++) { document.writeln(‘args[‘, i, ‘]: ‘, args[i], ‘<br>‘); } document.writeln(‘<br>‘);
// 上一級 foo = foo.caller; } }
// 運行測試 foo1(1, 2);
2. JavaScript面向?qū)ο蟮闹С?br>-------- 在前面的例子中其實已經(jīng)講到了object類型的“類型聲明”與“實例創(chuàng)建”。 在JavaScript中,,我們需要通過一個函數(shù)來聲明自己的object類型: //--------------------------------------------------------- // JavaScript中對象的類型聲明的形式代碼 // (以后的文檔中,,“對象名”通常用MyObject來替代) //--------------------------------------------------------- function 對象名(參數(shù)表) { this.屬性 = 初始值;
this.方法 = function(方法參數(shù)表) { // 方法實現(xiàn)代碼 } }
然后,我們可以通過這樣的代碼來創(chuàng)建這個對象類型的一個實例: //--------------------------------------------------------- // 創(chuàng)建實例的形式代碼 // (以后的文檔中,,“實例變量名”通常用obj來替代) //--------------------------------------------------------- var 實例變量名 = new 對象名(參數(shù)表);
接下來我們來看“對象”在JavaScript中的一些具體實現(xiàn)和奇怪特性,。
1). 函數(shù)在JavaScript的面向?qū)ο髾C(jī)制中的五重身份 ------ “對象名”——如MyObject()——這個函數(shù)充當(dāng)了以下語言角色: (1) 普通函數(shù) (2) 類型聲明 (3) 類型的實現(xiàn) (4) 類引用 (5) 對象的構(gòu)造函數(shù)
一些程序員(例如Delphi程序員)習(xí)慣于類型聲明與實現(xiàn)分開。例如在delphi 中,,Interface節(jié)用于聲明類型或者變量,,而implementation節(jié)用于書寫類型 的實現(xiàn)代碼,或者一些用于執(zhí)行的函數(shù),、代碼流程,。
但在JavaScript中,類型的聲明與實現(xiàn)是混在一起的,。一個對象的類型(類) 通過函數(shù)來聲明,,this.xxxx表明了該對象可具有的屬性或者方法。
這個函數(shù)的同時也是“類引用”,。在JavaScript,,如果你需要識別一個對象 的具體型別,你需要執(zhí)有一個“類引用”,?!?dāng)然,也就是這個函數(shù)的名 字,。instanceof 運算符就用于識別實例的類型,,我們來看一下它的應(yīng)用: //--------------------------------------------------------- // JavaScript中對象的類型識別 // 語法: 對象實例 instanceof 類引用 //--------------------------------------------------------- function MyObject() { this.data = ‘test data‘; }
// 這里MyObject()作為構(gòu)造函數(shù)使用 var obj = new MyObject(); var arr = new Array();
// 這里MyObject作為類引用使用 document.writeln(obj instanceof MyObject); document.writeln(arr instanceof MyObject);
================ (未完待續(xù)) ================ 接下來的內(nèi)容:
2. JavaScript面向?qū)ο蟮闹С?br>--------
2). 反射機(jī)制在JavaScript中的實現(xiàn) 3). this與with關(guān)鍵字的使用 4). 使用in關(guān)鍵字的運算 5). 使用instanceof關(guān)鍵字的運算 6). 其它與面向?qū)ο笙嚓P(guān)的關(guān)鍵字
3. 構(gòu)造與析構(gòu)
4. 實例和實例引用
5. 原型問題
6. 函數(shù)的上下文環(huán)境
2). 反射機(jī)制在JavaScript中的實現(xiàn) ------ JavaScript中通過for..in語法來實現(xiàn)了反射機(jī)制。但是JavaScript中并不 明確區(qū)分“屬性”與“方法”,以及“事件”,。因此,,對屬性的類型考查在JS 中是個問題。下面的代碼簡單示例for..in的使用與屬性識別: //--------------------------------------------------------- // JavaScript中for..in的使用和屬性識別 //--------------------------------------------------------- var _r_event = _r_event = /^[Oo]n.*/; var colorSetting = { method: ‘red‘, event: ‘blue‘, property: ‘‘ }
var obj2 = { a_method : function() {}, a_property: 1, onclick: undefined }
function propertyKind(obj, p) { return (_r_event.test(p) && (obj[p]==undefined || typeof(obj[p])==‘function‘)) ? ‘event‘ : (typeof(obj[p])==‘function‘) ? ‘method‘ : ‘property‘; }
var objectArr = [‘window‘, ‘obj2‘];
for (var i=0; i<objectArr.length; i++) { document.writeln(‘<p>for ‘, objectArr[i], ‘<hr>‘);
var obj = eval(objectArr[i]); for (var p in obj) { var kind = propertyKind(obj, p); document.writeln(‘obj.‘, p, ‘ is a ‘, kind.fontcolor(colorSetting[kind]), ‘: ‘, obj[p], ‘<br>‘); }
document.writeln(‘</p>‘); }
一個常常被開發(fā)者忽略的事實是:JavaScript本身是沒有事件(Event)系統(tǒng)的,。通 常我們在JavaScript用到的onclick等事件,,其實是IE的DOM模型提供的。從更內(nèi)核 的角度上講:IE通過COM的接口屬性公布了一組事件接口給DOM,。
有兩個原因,,使得在JS中不能很好的識別“一個屬性是不是事件”: - COM接口中本身只有方法,屬性與事件,,都是通過一組get/set方法來公布的,。 - JavaScript中,本身并沒有獨立的“事件”機(jī)制,。
因此我們看到event的識別方法,,是檢測屬性名是否是以‘on‘字符串開頭(以‘On‘開 頭的是Qomo的約定)。接下來,,由于DOM對象中的事件是可以不指定處理函數(shù)的,,這 種情況下事件句柄為null值(Qomo采用相同的約定);在另外的一些情況下,,用戶可 能象obj2這樣,,定義一個值為 undefined的事件。因此“事件”的判定條件被處理 成一個復(fù)雜的表達(dá)式: ("屬性以on/On開頭" && ("值為null/undefined" || "類型為function"))
另外,,從上面的這段代碼的運行結(jié)果來看,。對DOM對象使用for..in,是不能列舉出 對象方法來的,。
最后說明一點,。事實上,在很多語言的實現(xiàn)中,,“事件”都不是“面向?qū)ο?#8221;的語 言特性,,而是由具體的編程模型來提供的。例如Delphi中的事件驅(qū)動機(jī)制,,是由Win32 操作系統(tǒng)中的窗口消息機(jī)制來提供,,或者由用戶代碼在Component/Class中主動調(diào)用 事件處理函數(shù)來實現(xiàn)。
“事件”是一個“如何驅(qū)動編程模型”的機(jī)制/問題,,而不是語言本身的問題,。然 而以PME(property/method/event)為框架的OOP概念,,已經(jīng)深入人心,,所以當(dāng)編程語 言或系統(tǒng)表現(xiàn)出這些特性來的時候,就已經(jīng)沒人關(guān)心“event究竟是誰實現(xiàn)”的了。
3). this與with關(guān)鍵字的使用 ------ 在JavaScript的對象系統(tǒng)中,,this關(guān)鍵字用在兩種地方: - 在構(gòu)造器函數(shù)中,,指代新創(chuàng)建的對象實例 - 在對象的方法被調(diào)用時,指代調(diào)用該方法的對象實例
如果一個函數(shù)被作為普通函數(shù)(而不是對象方法)調(diào)用,,那么在函數(shù)中的this關(guān)鍵字 將指向window對象,。與此相同的,如果this關(guān)鍵字不在任何函數(shù)中,,那么他也指向 window對象,。
由于在JavaScript中不明確區(qū)分函數(shù)與方法。因此有些代碼看起來很奇怪: //--------------------------------------------------------- // 函數(shù)的幾種可能調(diào)用形式 //--------------------------------------------------------- function foo() { // 下面的this指代調(diào)用該方法的對象實例 if (this===window) { document.write(‘call a function.‘, ‘<BR>‘); } else { document.write(‘call a method, by object: ‘, this.name, ‘<BR>‘); } }
function MyObject(name) { // 下面的this指代new關(guān)鍵字新創(chuàng)建實例 this.name = name; this.foo = foo; }
var obj1 = new MyObject(‘obj1‘); var obj2 = new MyObject(‘obj2‘);
// 測試1: 作為函數(shù)調(diào)用 foo();
// 測試2: 作為對象方法的調(diào)用 obj1.foo(); obj2.foo();
// 測試3: 將函數(shù)作為“指定對象的”方法調(diào)用 foo.call(obj1); foo.apply(obj2);
在上面的代碼里,,obj1/obj2對foo()的調(diào)用是很普通的調(diào)用方法,。——也就 是在構(gòu)造器上,,將一個函數(shù)指定為對象的方法,。
而測試3中的call()與apply()就比較特殊。
在這個測試中,,foo()仍然作為普通函數(shù)來調(diào)用,,只是JavaScript的語言特性 允許在call()/apply()時,傳入一個對象實例來指定foo()的上下文環(huán)境中所 出現(xiàn)的this關(guān)鍵字的引用,?!枰⒁獾氖牵藭r的foo()仍舊是一個普通 函數(shù)調(diào)用,,而不是對象方法調(diào)用,。
與this“指示調(diào)用該方法的對象實例”有些類同的,with()語法也用于限定 “在一段代碼片段中默認(rèn)使用對象實例”,?!绻皇褂脀ith()語法,那 么這段代碼將受到更外層with()語句的影響,;如果沒有更外層的with(),,那 么這段代碼的“默認(rèn)使用的對象實例”將是window。
然而需要注意的是this與with關(guān)鍵字不是互為影響的,。如下面的代碼: //--------------------------------------------------------- // 測試: this與with關(guān)鍵字不是互為影響的 //--------------------------------------------------------- function test() { with (obj2) { this.value = 8; } } var obj2 = new Object(); obj2.value = 10;
test(); document.writeln(‘obj2.value: ‘, obj2.value, ‘<br>‘); document.writeln(‘window.value: ‘, window.value, ‘<br>‘);
你不能指望這樣的代碼在調(diào)用結(jié)束后,,會使obj2.value屬性置值為8。這幾行 代碼的結(jié)果是:window對象多了一個value屬性,,并且值為8,。
with(obj){...}這個語法,只能限定對obj的既有屬性的讀取,,而不能主動的 聲明它,。一旦with()里的對象沒有指定的屬性,或者with()限定了一個不是對 象的數(shù)據(jù),那么結(jié)果會產(chǎn)生一個異常,。
4). 使用in關(guān)鍵字的運算 ------ 除了用for..in來反射對象的成員信息之外,,JavaScript中也允許直接用in 關(guān)鍵字去檢測對象是否有指定名字的屬性。
in關(guān)鍵字經(jīng)常被提及的原因并不是它檢測屬性是否存在的能力,,因此在早期 的代碼中,,很多可喜歡用“if (!obj.propName) {}” 這樣的方式來檢測propName 是否是有效的屬性?!芏鄷r候,,檢測有效性比檢測“是否存有該屬性”更 有實用性。因此這種情況下,,in只是一個可選的,、官方的方案。
in關(guān)鍵字的重要應(yīng)用是高速字符串檢索,。尤其是在只需要判定“字符串是否 存在”的情況下,。例如10萬個字符串,如果存儲在數(shù)組中,,那么檢索效率將會 極差,。 //--------------------------------------------------------- // 使用對象來檢索 //--------------------------------------------------------- function arrayToObject(arr) { for (var obj=new Object(), i=0, imax=arr.length; i<imax; i++) { obj[arr[i]]=null; } return obj; }
var arr = [‘abc‘, ‘def‘, ‘ghi‘]; // more and more... obj = arrayToObject(arr);
function valueInArray(v) { for (var i=0, imax=arr.length; i<imax; i++) { if (arr[i]==v) return true; }
return false; }
function valueInObject(v) { return v in obj; }
這種使用關(guān)鍵字in的方法,也存在一些限制,。例如只能查找字符串,,而數(shù) 組元素可以是任意值。另外,,arrayToObject()也存在一些開銷,,這使得它 不適合于頻繁變動的查找集。最后,,(我想你可能已經(jīng)注意到了)使用對象 來查找的時候并不能準(zhǔn)確定位到查找數(shù)據(jù),,而數(shù)組中可以指向結(jié)果的下標(biāo)。 ~~~~~~~~~~~~~~~~~~ (續(xù))
2. JavaScript面向?qū)ο蟮闹С?br>-------- (續(xù))
5). 使用instanceof關(guān)鍵字的運算 ------ 在JavaScript中提供了instanceof關(guān)鍵字來檢測實例的類型,。這在前面討 論它的“五重身份”時已經(jīng)講過,。但instanceof的問題是,它總是列舉整個 原型鏈以檢測類型(關(guān)于原型繼承的原理在“構(gòu)造與析構(gòu)”小節(jié)講述),,如: //--------------------------------------------------------- // instanceof使用中的問題 //--------------------------------------------------------- function MyObject() { // ... }
function MyObject2() { // ... } MyObject2.prototype = new MyObject();
obj1 = new MyObject(); obj2 = new MyObject2();
document.writeln(obj1 instanceof MyObject, ‘<BR>‘); document.writeln(obj2 instanceof MyObject, ‘<BR>‘);
我們看到,,obj1與obj2都是MyObject的實例,但他們是不同的構(gòu)造函數(shù)產(chǎn)生 的,?!⒁猓@在面向?qū)ο罄碚撝姓_的:因為obj2是MyObject的子類實 例,,因此它具有與obj1相同的特性,。在應(yīng)用中這是obj2的多態(tài)性的體現(xiàn)之一,。
但是,即便如此,,我們也必須面臨這樣的問題:如何知道obj2與obj1是否是 相同類型的實例呢?——也就是說,,連構(gòu)造器都相同,?
instanceof關(guān)鍵字不提供這樣的機(jī)制。一個提供實現(xiàn)這種檢測的能力的,,是 Object.constructor屬性,。——但請先記住,,它的使用遠(yuǎn)比你想象的要難,。
好的,問題先到這里,。constructor屬性已經(jīng)涉及到“構(gòu)造與析構(gòu)”的問題,, 這個我們后面再講。“原型繼承”,、“構(gòu)造與析構(gòu)”是JavaScript的OOP中 的主要問題,、核心問題,以及“致命問題”,。
6). null與undefined ------ 在JavaScript中,,null與undefined曾一度使我迷惑。下面的文字,,有利于 你更清晰的認(rèn)知它(或者讓你更迷惑): - null是關(guān)鍵字,;undefined是Global對象的一個屬性。 - null是對象(空對象, 沒有任何屬性和方法),;undefined是undefined類 型的值,。試試下面的代碼: document.writeln(typeof null); document.writeln(typeof undefined); - 對象模型中,所有的對象都是Object或其子類的實例,,但null對象例外: document.writeln(null instanceof Object); - null“等值(==)”于undefined,,但不“全等值(===)”于undefined: document.writeln(null == undefined); document.writeln(null == undefined); - 運算時null與undefined都可以被類型轉(zhuǎn)換為false,但不等值于false: document.writeln(!null, !undefined); document.writeln(null==false); document.writeln(undefined==false); ~~~~~~~~~~~~~~~~~~ (續(xù))
3. 構(gòu)造,、析構(gòu)與原型問題 -------- 我們已經(jīng)知道一個對象是需要通過構(gòu)造器函數(shù)來產(chǎn)生的,。我們先記住幾點: - 構(gòu)造器是一個普通的函數(shù) - 原型是一個對象實例 - 構(gòu)造器有原型屬性,對象實例沒有 - (如果正常地實現(xiàn)繼承模型,,)對象實例的constructor屬性指向構(gòu)造器 - 從三,、四條推出:obj.constructor.prototype指向該對象的原型
好,我們接下來分析一個例子,,來說明JavaScript的“繼承原型”聲明,,以 及構(gòu)造過程,。 //--------------------------------------------------------- // 理解原型、構(gòu)造,、繼承的示例 //--------------------------------------------------------- function MyObject() { this.v1 = ‘abc‘; }
function MyObject2() { this.v2 = ‘def‘; } MyObject2.prototype = new MyObject();
var obj1 = new MyObject(); var obj2 = new MyObject2();
1). new()關(guān)鍵字的形式化代碼 ------ 我們先來看“obj1 = new MyObject()”這行代碼中的這個new關(guān)鍵字,。
new關(guān)鍵字用于產(chǎn)生一個新的實例(說到這里補(bǔ)充一下,我習(xí)慣于把保留字叫關(guān)鍵 字,。另外,,在JavaScript中new關(guān)鍵字同時也是一個運算符),這個實例的缺省屬性 中,,(至少)會執(zhí)有構(gòu)造器函數(shù)的原型屬性(prototype)的一個引用(在ECMA Javascript 規(guī)范中,,對象的這個屬性名定義為__proto__)。
每一個函數(shù),,無論它是否用作構(gòu)造器,,都會有一個獨一無二的原型對象(prototype)。 對于JavaScript“內(nèi)置對象的構(gòu)造器”來說,,它指向內(nèi)部的一個原型,。缺省時JavaScript 構(gòu)造出一個“空的初始對象實例(不是null)”并使原型引用指向它。然而如果你給函 數(shù)的這個prototype賦一個新的對象,,那么新的對象實例將執(zhí)有它的一個引用,。
接下來,構(gòu)造過程將調(diào)用MyObject()來完成初始化,?!⒁猓@里只是“初始 化”,。
為了清楚地解釋這個過程,,我用代碼形式化地描述一下這個過程: //--------------------------------------------------------- // new()關(guān)鍵字的形式化代碼 //--------------------------------------------------------- function new(aFunction) { // 基本對象實例 var _this = {};
// 原型引用 var _proto= aFunction.prototype;
/* if compat ECMA Script _this.__proto__ = _proto; */
// 為存取原型中的屬性添加(內(nèi)部的)getter _this._js_GetAttributes= function(name) { if (_existAttribute.call(this, name)) return this[name] else if (_js_LookupProperty.call(_proto, name)) retrun OBJ_GET_ATTRIBUTES.call(_proto, name) else return undefined; }
// 為存取原型中的屬性添加(內(nèi)部的)setter _this._js_GetAttributes = function(name, value) { if (_existAttribute.call(this, name)) this[name] = value else if (OBJ_GET_ATTRIBUTES.call(_proto, name) !== value) { this[name] = value // 創(chuàng)建當(dāng)前實例的新成員 } }
// 調(diào)用構(gòu)造函數(shù)完成初始化, (如果有,)傳入args aFunction.call(_this);
// 返回對象 return _this; }
所以我們看到以下兩點: - 構(gòu)造函數(shù)(aFunction)本身只是對傳入的this實例做“初始化”處理,而 不是構(gòu)造一個對象實例,。 - 構(gòu)造的過程實際發(fā)生在new()關(guān)鍵字/運算符的內(nèi)部,。
而且,構(gòu)造函數(shù)(aFunction)本身并不需要操作prototype,,也不需要回傳this,。
2). 由用戶代碼維護(hù)的原型(prototype)鏈 ------ 接下來我們更深入的討論原型鏈與構(gòu)造過程的問題。這就是: - 原型鏈?zhǔn)怯脩舸a創(chuàng)建的,,new()關(guān)鍵字并不協(xié)助維護(hù)原型鏈
以Delphi代碼為例,,我們在聲明繼承關(guān)系的時候,可以用這樣的代碼: //--------------------------------------------------------- // delphi中使用的“類”類型聲明 //--------------------------------------------------------- type TAnimal = class(TObject); // 動物 TMammal = class(TAnimal); // 哺乳動物 TCanine = class(TMammal); // 犬科的哺乳動物 TDog = class(TCanine); // 狗
這時,,Delphi的編譯器會通過編譯技術(shù)來維護(hù)一個繼承關(guān)系鏈表,。我們可以通 過類似以下的代碼來查詢這個鏈表: //--------------------------------------------------------- // delphi中使用繼關(guān)系鏈表的關(guān)鍵代碼 //--------------------------------------------------------- function isAnimal(obj: TObject): boolean; begin Result := obj is TAnimal; end;
var dog := TDog;
// ... dog := TDog.Create(); writeln(isAnimal(dog));
可以看到,在Delphi的用戶代碼中,,不需要直接繼護(hù)繼承關(guān)系的鏈表,。這是因 為Delphi是強(qiáng)類型語言,,在處理用class()關(guān)鍵字聲明類型時,delphi的編譯器 已經(jīng)為用戶構(gòu)造了這個繼承關(guān)系鏈,?!⒁猓@個過程是聲明,,而不是執(zhí)行 代碼,。
而在JavaScript中,如果需要獲知對象“是否是某個基類的子類對象”,,那么 你需要手工的來維護(hù)(與delphi這個例子類似的)一個鏈表,。當(dāng)然,,這個鏈表不 叫類型繼承樹,,而叫“(對象的)原型鏈表”?!贘S中,,沒有“類”類型。
參考前面的JS和Delphi代碼,,一個類同的例子是這樣: //--------------------------------------------------------- // JS中“原型鏈表”的關(guān)鍵代碼 //--------------------------------------------------------- // 1. 構(gòu)造器 function Animal() {}; function Mammal() {}; function Canine() {}; function Dog() {};
// 2. 原型鏈表 Mammal.prototype = new Animal(); Canine.prototype = new Mammal(); Dog.prototype = new Canine();
// 3. 示例函數(shù) function isAnimal(obj) { return obj instanceof Animal; }
var dog = new Dog(); document.writeln(isAnimal(dog));
可以看到,,在JS的用戶代碼中,“原型鏈表”的構(gòu)建方法是一行代碼: "當(dāng)前類的構(gòu)造器函數(shù)".prototype = "直接父類的實例"
這與Delphi一類的語言不同:維護(hù)原型鏈的實質(zhì)是在執(zhí)行代碼,,而非聲明,。
那么,“是執(zhí)行而非聲明”到底有什么意義呢,?
JavaScript是會有編譯過程的,。這個過程主要處理的是“語法檢錯”、“語 法聲明”和“條件編譯指令”,。而這里的“語法聲明”,,主要處理的就是函 數(shù)聲明?!@也是我說“函數(shù)是第一類的,,而對象不是”的一個原因。
如下例: //--------------------------------------------------------- // 函數(shù)聲明與執(zhí)行語句的關(guān)系(firefox 兼容) //--------------------------------------------------------- // 1. 輸出1234 testFoo(1234);
// 2. 嘗試輸出obj1 // 3. 嘗試輸出obj2 testFoo(obj1); try { testFoo(obj2); } catch(e) { document.writeln(‘Exception: ‘, e.description, ‘<BR>‘); }
// 聲明testFoo() function testFoo(v) { document.writeln(v, ‘<BR>‘); }
// 聲明object var obj1 = {}; obj2 = { toString: function() {return ‘hi, object.‘} }
// 4. 輸出obj1 // 5. 輸出obj2 testFoo(obj1); testFoo(obj2);
這個示例代碼在JS環(huán)境中執(zhí)行的結(jié)果是: ------------------------------------ 1234 undefined Exception: ‘obj2‘ 未定義 [object Object] hi, obj ------------------------------------ 問題是,,testFoo()是在它被聲明之前被執(zhí)行的,;而同樣用“直接聲明”的 形式定義的object變量,卻不能在聲明之前引用,?!又校诙?、三 個輸入是不正確的,。
函數(shù)可以在聲明之前引用,,而其它類型的數(shù)值必須在聲明之后才能被使用。 這說明“聲明”與“執(zhí)行期引用”在JavaScript中是兩個過程,。
另外我們也可以發(fā)現(xiàn),,使用"var"來聲明的時候,編譯器會先確認(rèn)有該變量 存在,,但變量的值會是“undefined”,。——因此“testFoo(obj1)”不會發(fā) 生異常,。但是,,只有等到關(guān)于obj1的賦值語句被執(zhí)行過,才會有正常的輸出,。 請對照第二,、三與第四、五行輸出的差異,。
由于JavaScript對原型鏈的維護(hù)是“執(zhí)行”而不是“聲明”,,這說明“原型 鏈?zhǔn)怯捎脩舸a來維護(hù)的,而不是編譯器維護(hù)的,。
由這個推論,,我們來看下面這個例子: //--------------------------------------------------------- // 示例:錯誤的原型鏈 //--------------------------------------------------------- // 1. 構(gòu)造器 function Animal() {}; // 動物 function Mammal() {}; // 哺乳動物 function Canine() {}; // 犬科的哺乳動物
// 2. 構(gòu)造原型鏈 var instance = new Mammal(); Mammal.prototype = new Animal(); Canine.prototype = instance;
// 3. 測試輸出 var obj = new Canine(); document.writeln(obj instanceof Animal);
這個輸出結(jié)果,使我們看到一個錯誤的原型鏈導(dǎo)致的結(jié)果“犬科的哺乳動 物‘不是’一種動物”,。
根源在于“2. 構(gòu)造原型鏈”下面的幾行代碼是解釋執(zhí)行的,,而不是象var和 function那樣是“聲明”并在編譯期被理解的。解決問題的方法是修改那三 行代碼,,使得它的“執(zhí)行過程”符合邏輯: //--------------------------------------------------------- // 上例的修正代碼(部分) //--------------------------------------------------------- // 2. 構(gòu)造原型鏈 Mammal.prototype = new Animal(); var instance = new Mammal(); Canine.prototype = instance;
3). 原型實例是如何被構(gòu)造過程使用的 ------ 仍以Delphi為例,。構(gòu)造過程中,delphi中會首先創(chuàng)建一個指定實例大小的 “空的對象”,,然后逐一給屬性賦值,,以及調(diào)用構(gòu)造過程中的方法、觸發(fā)事 件等,。
JavaScript中的new()關(guān)鍵字中隱含的構(gòu)造過程,,與Delphi的構(gòu)造過程并不完全一致。但 在構(gòu)造器函數(shù)中發(fā)生的行為卻與上述的類似: //--------------------------------------------------------- // JS中的構(gòu)造過程(形式代碼) //--------------------------------------------------------- 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();
如果以單個類為參考對象的,,這個構(gòu)造過程中JavaScript可以擁有與Delphi 一樣豐富的行為,。然而,由于Delphi中的構(gòu)造過程是“動態(tài)的”,,因此事實上 Delphi還會調(diào)用父類(MyObject)的構(gòu)造過程,,以及觸發(fā)父類的OnCreate()事件。
JavaScript沒有這樣的特性,。父類的構(gòu)造過程僅僅發(fā)生在為原型(prototype 屬性)賦值的那一行代碼上,。其后,,無論有多少個new MyObject2()發(fā)生, MyObject()這個構(gòu)造器都不會被使用,?!@也意味著: - 構(gòu)造過程中,原型對象是一次性生成的,;新對象只持有這個原型實例的引用 (并用“寫復(fù)制”的機(jī)制來存取其屬性),,而并不再調(diào)用原型的構(gòu)造器。
由于不再調(diào)用父類的構(gòu)造器,,因此Delphi中的一些特性無法在JavaScript中實現(xiàn),。 這主要影響到構(gòu)造階段的一些事件和行為?!獰o法把一些“對象構(gòu)造過程中” 的代碼寫到父類的構(gòu)造器中,。因為無論子類構(gòu)造多少次,這次對象的構(gòu)造過程根 本不會激活父類構(gòu)造器中的代碼,。
JavaScript中屬性的存取是動態(tài)的,,因為對象存取父類屬性依賴于原型鏈表,,構(gòu)造 過程卻是靜態(tài)的,,并不訪問父類的構(gòu)造器;而在Delphi等一些編譯型語言中,,(不使 用讀寫器的)屬性的存取是靜態(tài)的,,而對象的構(gòu)造過程則動態(tài)地調(diào)用父類的構(gòu)造函數(shù)。 所以再一次請大家看清楚new()關(guān)鍵字的形式代碼中的這一行: //--------------------------------------------------------- // new()關(guān)鍵字的形式化代碼 //--------------------------------------------------------- function new(aFunction) { // 原型引用 var _proto= aFunction.prototype;
// ... }
這個過程中,,JavaScript做的是“get a prototype_Ref”,,而Delphi等其它語言做 的是“Inherited Create()”。
~~~~~~~~~~~~~~~~~~ (續(xù))
4). 需要用戶維護(hù)的另一個屬性:constructor ------ 回顧前面的內(nèi)容,,我們提到過: - (如果正常地實現(xiàn)繼承模型,,)對象實例的constructor屬性指向構(gòu)造器 - obj.constructor.prototype指向該對象的原型 - 通過Object.constructor屬性,可以檢測obj2與obj1是否是相同類型的實例
與原型鏈要通過用戶代碼來維護(hù)prototype屬性一樣,,實例的構(gòu)造器屬性constructor 也需要用戶代碼維護(hù),。
對于JavaScript的內(nèi)置對象來說,constructor屬性指向內(nèi)置的構(gòu)造器函數(shù),。如: //--------------------------------------------------------- // 內(nèi)置對象實例的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‘; }
// 測試數(shù)據(jù)和相關(guān)代碼 function MyObject() { } function MyObject2() { } MyObject2.prototype = new MyObject();
window.execScript(‘‘+ ‘Function CreateVBArray()‘ + ‘ Dim a(2, 2)‘ + ‘ CreateVBArray = a‘ + ‘End Function‘, ‘VBScript‘);
document.writeln(‘<div id=dom style="display:none">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), ‘<br>‘); document.writeln(objectTypes(ax), ‘<br>‘); document.writeln(objectTypes(obj), ‘<br>‘); document.writeln(objectTypes(obj2), ‘<br>‘); document.writeln(objectTypes(dom), ‘<br>‘);
在這個例子中,,我們發(fā)現(xiàn)constructor屬性被實現(xiàn)得并不完整。對于DOM對象,、ActiveX對象 來說這個屬性都沒有正確的返回,。
確切的說,DOM(包括Image)對象與ActiveX對象都不是標(biāo)準(zhǔn)JavaScript的對象體系中的,, 因此它們也可能會具有自己的constructor屬性,,并有著與JavaScript不同的解釋,。因此, JavaScript中不維護(hù)它們的constructor屬性,,是具有一定的合理性的,。
另外的一些單體對象(而非構(gòu)造器),也不具有constructor屬性,,例如“Math”和“Debug”,、 “Global”和“RegExp對象”。他們是JavaScript內(nèi)部構(gòu)造的,,不應(yīng)該公開構(gòu)造的細(xì)節(jié),。
我們也發(fā)現(xiàn)實例obj的constructor指向function MyObject()。這說明JavaScript維護(hù)了對 象的constructor屬性,?!@與一些人想象的不一樣。
然而再接下來,,我們發(fā)現(xiàn)MyObject2()的實例obj2的constructor仍然指向function MyObject(),。 盡管這很說不通,然而現(xiàn)實的確如此,?!@到底是為什么呢?
事實上,,僅下面的代碼: -------- function MyObject2() { }
obj2 = new MyObject2(); document.writeln(MyObject2.prototype.constructor === MyObject2); -------- 構(gòu)造的obj2.constructor將正確的指向function MyObject2(),。事實上,我們也會注意到這 種情況下,,MyObject2的原型屬性的constructor也正確的指向該函數(shù),。然而,由于JavaScript 要求指定prototype對象來構(gòu)造原型鏈: -------- function MyObject2() { } MyObject2.prototype = new MyObject();
obj2 = new MyObject2(); -------- 這時,,再訪問obj2,,將會得到新的原型(也就是MyObject2.prototype)的constructor屬性。 因此,,一切很明了:原型的屬性影響到構(gòu)造過程對對象的constructor的初始設(shè)定,。
作為一種補(bǔ)充的解決問題的手段,JavaScript開發(fā)規(guī)范中說“need to remember to reset the constructor property‘,,要求用戶自行設(shè)定該屬性,。
所以你會看到更規(guī)范的JavaScript代碼要求這樣書寫: //--------------------------------------------------------- // 維護(hù)constructor屬性的規(guī)范代碼 //--------------------------------------------------------- function MyObject2() { } MyObject2.prototype = new MyObject(); MyObject2.prototype.constructor = MyObject2;
obj2 = new MyObject2();
更外一種解決問題的方法,是在function MyObject()中去重置該值,。當(dāng)然,,這樣會使 得執(zhí)行效率稍低一點點: //--------------------------------------------------------- // 維護(hù)constructor屬性的第二種方式 //--------------------------------------------------------- function MyObject2() { this.constructor = arguments.callee; // or, this.constructor = MyObject2;
// ... } MyObject2.prototype = new MyObject();
obj2 = new MyObject2();
5). 析構(gòu)問題 ------ JavaScript中沒有析構(gòu)函數(shù),但卻有“對象析構(gòu)”的問題。也就是說,,盡管我們不 知道一個對象什么時候會被析構(gòu),,也不能截獲它的析構(gòu)過程并處理一些事務(wù)。然而,, 在一些不多見的時候,,我們會遇到“要求一個對象立即析構(gòu)”的問題。
問題大多數(shù)的時候出現(xiàn)在對ActiveX Object的處理上,。因為我們可能在JavaScript 里創(chuàng)建了一個ActiveX Object,,在做完一些處理之后,,我們又需要再創(chuàng)建一個。而 如果原來的對象供應(yīng)者(Server)不允許創(chuàng)建多個實例,那么我們就需要在JavaScript 中確保先前的實例是已經(jīng)被釋放過了,。接下來,,即使Server允許創(chuàng)建多個實例,,而 在多個實例間允許共享數(shù)據(jù)(例如OS的授權(quán),,或者資源、文件的鎖),,那么我們在新 實例中的操作就可能會出問題,。
可能還是有人不明白我們在說什么,那么我就舉一個例子:如果創(chuàng)建一個Excel對象,, 打開文件A,,然后我們save它,然后關(guān)閉這個實例,。然后我們再創(chuàng)建Excel對象并打開 同一文件,?!⒁膺@時JavaScript可能還沒有來得及析構(gòu)前一個對象,。——這時我們 再想Save這個文件,,就發(fā)現(xiàn)失敗了,。下面的代碼示例這種情況: //--------------------------------------------------------- // JavaScript中的析構(gòu)問題(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()">創(chuàng)建</button> <button onclick="writeXLS()">重寫</button> </body>
在這個例子中,在本地文件操作時并不會出現(xiàn)異常,?!疃嘀皇怯幸恍﹥?nèi)存垃 圾而已。然而,,如果strSaveLocation是一個遠(yuǎn)程的URL,,這時本地將會保存一個 文件存取權(quán)限的憑證,而且同時只能一個(遠(yuǎn)程的)實例來開啟該excel文檔并存 儲,。于是如果反復(fù)點擊"重寫"按鈕,,就會出現(xiàn)異常。
——注意,這是在SPS中操作共享文件時的一個實例的簡化代碼,。因此,,它并非 “學(xué)術(shù)的”無聊討論,而且工程中的實際問題,。
解決這個問題的方法很復(fù)雜,。它涉及到兩個問題: - 本地憑證的釋放 - ActiveX Object實例的釋放
下面我們先從JavaScript中對象的“失效”問題說起。簡單的說: - 一個對象在其生存的上下文環(huán)境之外,,即會失效,。 - 一個全局的對象在沒有被執(zhí)用(引用)的情況下,即會失效,。
例如: //--------------------------------------------------------- // 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”在函數(shù)testObject()中構(gòu)造了_obj1,,但是在函數(shù)退出時, 它就已經(jīng)離開了函數(shù)的上下文環(huán)境,,因此_obj1失效了,; - “示例2”中,testObject2()中也構(gòu)造了一個對象_obj2并傳出,,因 此對象有了“函數(shù)外”的上下文環(huán)境(和生存周期),,然而由于函數(shù) 的返回值沒有被其它變量“持有”,因此_obj2也立即失效了,; - “示例3”中,,testObject2()構(gòu)造的_obj2被外部的變量obj3持用了, 這時,,直到“obj3=null”這行代碼生效時,,_obj2才會因為引用關(guān)系 消失而失效。 - 與示例3相同的原因,,“示例4”中的_obj2會在“arr=[]”這行代碼 之后才會失效,。
但是,對象的“失效”并不等會“釋放”,。在JavaScript運行環(huán)境的內(nèi)部,,沒 有任何方式來確切地告訴用戶“對象什么時候會釋放”。這依賴于JavaScript 的內(nèi)存回收機(jī)制,?!@種策略與.NET中的回收機(jī)制是類同的。
在前面的Excel操作示例代碼中,,對象的所有者,,也就是"EXCEL.EXE"這個進(jìn)程 只能在“ActiveX Object實例的釋放”之后才會發(fā)生。而文件的鎖,,以及操作 系統(tǒng)的權(quán)限憑證是與進(jìn)程相關(guān)的,。因此如果對象僅是“失效”而不是“釋放”,, 那么其它進(jìn)程處理文件和引用操作系統(tǒng)的權(quán)限憑據(jù)時就會出問題。
——有些人說這是JavaScript或者COM機(jī)制的BUG,。其實不是,,這是OS、IE 和JavaScript之間的一種復(fù)雜關(guān)系所導(dǎo)致的,,而非獨立的問題,。
Microsoft公開了解決這種問題的策略:主動調(diào)用內(nèi)存回收過程。
在(微軟的)JScript中提供了一個CollectGarbage()過程(通常簡稱GC過程),, GC過程用于清理當(dāng)前IE中的“失效的對象失例”,,也就是調(diào)用對象的析構(gòu)過程。
在上例中調(diào)用GC過程的代碼是: //--------------------------------------------------------- // 處理ActiveX Object時,,GC過程的標(biāo)準(zhǔn)調(diào)用方式 //--------------------------------------------------------- function writeXLS() { //(略...)
excel.Quit(); excel = null; setTimeout(CollectGarbage, 1); }
第一行代碼調(diào)用excel.Quit()方法來使得excel進(jìn)程中止并退出,,這時由于JavaScript 環(huán)境執(zhí)有excel對象實例,因此excel進(jìn)程并不實際中止,。
第二行代碼使excel為null,,以清除對象引用,從而使對象“失效”,。然而由于 對象仍舊在函數(shù)上下文環(huán)境中,,因此如果直接調(diào)用GC過程,對象仍然不會被清理,。
第三行代碼使用setTimeout()來調(diào)用CollectGarbage函數(shù),,時間間隔設(shè)為‘1‘,只 是使得GC過程發(fā)生在writeXLS()函數(shù)執(zhí)行完之后,。這樣excel對象就滿足了“能被 GC清理”的兩個條件:沒有引用和離開上下文環(huán)境,。
GC過程的使用,在使用了ActiveX Object的JS環(huán)境中很有效,。一些潛在的ActiveX Object包括XML,、VML、OWC(Office Web Componet),、flash,,甚至包括在JS中的VBArray。 從這一點來看,,ajax架構(gòu)由于采用了XMLHTTP,并且同時要滿足“不切換頁面”的 特性,,因此在適當(dāng)?shù)臅r候主動調(diào)用GC過程,,會得到更好的效率用UI體驗。
事實上,,即使使用GC過程,,前面提到的excel問題仍然不會被完全解決。因為IE還 緩存了權(quán)限憑據(jù)。使頁的權(quán)限憑據(jù)被更新的唯一方法,,只能是“切換到新的頁面”,, 因此事實上在前面提到的那個SPS項目中,我采用的方法并不是GC,,而是下面這一 段代碼: //--------------------------------------------------------- // 處理ActiveX Object時采用的頁面切換代碼 //--------------------------------------------------------- function writeXLS() { //(略...)
excel.Quit(); excel = null; // 下面代碼用于解決IE call Excel的一個BUG, MSDN中提供的方法: // setTimeout(CollectGarbage, 1); // 由于不能清除(或同步)網(wǎng)頁的受信任狀態(tài), 所以將導(dǎo)致SaveAs()等方法在 // 下次調(diào)用時無效. location.reload(); }
最后之最后,,關(guān)于GC的一個補(bǔ)充說明:在IE窗體被最小化時,IE將會主動調(diào)用一次 CollectGarbage()函數(shù),。這使得IE窗口在最小化之后,,內(nèi)存占用會有明顯改善。
~~~~~~~~~~~~~~~~~~ (續(xù))
4. 實例和實例引用 -------- 在.NET Framework對CTS(Common Type System)約定“一切都是對象”,,并分為“值 類型”和“引用類型”兩種,。其中“值類型”的對象在轉(zhuǎn)換成“引用類型”數(shù)據(jù)的 過程中,需要進(jìn)行一個“裝箱”和“拆箱”的過程,。
在JavaScript也有同樣的問題,。我們看到的typeof關(guān)鍵字,返回以下六種數(shù)據(jù)類型: "number",、"string",、"boolean"、"object",、"function" 和 "undefined",。
我們也發(fā)現(xiàn)JavaScript的對象系統(tǒng)中,有String,、Number,、Function、Boolean這四 種對象構(gòu)造器,。那么,,我們的問題是:如果有一個數(shù)字A,typeof(A)的結(jié)果,,到底會 是‘number‘呢,,還是一個構(gòu)造器指向function Number()的對象呢?
//--------------------------------------------------------- // 關(guān)于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]);
測試代碼的執(zhí)行結(jié)果如下: ----------- A1 is Value, type of number A2 is Object, construct by function Number() { [native code] } true,true -----------
我們注意到,,A1和A2的構(gòu)造器都指向Number,。這意味著通過constructor屬性來識別 對象,(有時)比typeof更加有效,。因為“值類型數(shù)據(jù)”A1作為一個對象來看待時,, 與A2有完全相同的特性。
——除了與實例引用有關(guān)的問題,。
參考JScript手冊,,我們對其它基礎(chǔ)類型和構(gòu)造器做相同考察,,可以發(fā)現(xiàn): - 基礎(chǔ)類型中的undefined、number,、boolean和string,,是“值類型”變量 - 基礎(chǔ)類型中的array、function和object,,是“引用類型”變量 - 使用new()方法構(gòu)造出對象,,是“引用類型”變量
下面的代碼說明“值類型”與“引用類型”之間的區(qū)別: //--------------------------------------------------------- // 關(guān)于JavaScript類型系統(tǒng)中的值/引用問題 //--------------------------------------------------------- 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]);
測試代碼的執(zhí)行結(jié)果如下: ----------- true, true false, false -----------
我們看到,無論是等值運算(==),,還是全等運算(===),,對“對象”和“值”的 理解都是不一樣的。
更進(jìn)一步的理解這種現(xiàn)象,,我們知道: - 運算結(jié)果為值類型,,或變量為值類型時,等值(或全等)比較可以得到預(yù)想結(jié)果 - (即使包含相同的數(shù)據(jù),,)不同的對象實例之間是不等值(或全等)的 - 同一個對象的不同引用之間,,是等值(==)且全等(===)的
但對于String類型,有一點補(bǔ)充:根據(jù)JScript的描述,,兩個字符串比較時,,只要有 一個是值類型,則按值比較,。這意味著在上面的例子中,,代碼“str1==obj1”會得到 結(jié)果true。而全等(===)運算需要檢測變量類型的一致性,,因此“str1===obj1”的結(jié) 果返回false,。
JavaScript中的函數(shù)參數(shù)總是傳入值參,引用類型(的實例)是作為指針值傳入的,。因此 函數(shù)可以隨意重寫入口變量,,而不用擔(dān)心外部變量被修改。但是,,需要留意傳入的引用 類型的變量,,因為對它方法調(diào)用和屬性讀寫可能會影響到實例本身?!?,也可以通 過引用類型的參數(shù)來傳出數(shù)據(jù)。
最后補(bǔ)充說明一下,,值類型比較會逐字節(jié)檢測對象實例中的數(shù)據(jù),,效率低但準(zhǔn)確性高; 而引用類型只檢測實例指針和數(shù)據(jù)類型,,因此效率高而準(zhǔn)確性低,。如果你需要檢測兩個 引用類型是否真的包含相同的數(shù)據(jù),可能你需要嘗試把它轉(zhuǎn)換成“字符串值”再來比較,。
6. 函數(shù)的上下文環(huán)境 -------- 只要寫過代碼,,你應(yīng)該知道變量是有“全局變量”和“局部變量”之分的。絕大多數(shù)的 JavaScript程序員也知道下面這些概念: //--------------------------------------------------------- // JavaScript中的全局變量與局部變量 //--------------------------------------------------------- var v1 = ‘全局變量-1‘; v2 = ‘全局變量-2‘;
function foo() { v3 = ‘全局變量-3‘;
var v4 = ‘只有在函數(shù)內(nèi)部并使用var定義的,,才是局部變量‘; }
按照通常對語言的理解來說,,不同的代碼調(diào)用函數(shù),都會擁有一套獨立的局部變量,。 因此下面這段代碼很容易理解: //--------------------------------------------------------- // 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());
結(jié)果顯示false,,表明不同(實例的方法)調(diào)用返回的局部變量“obj1/obj2”是不相同。
變量的局部,、全局特性與OOP的封裝性中的“私有(private)”,、“公開(public)”具 有類同性。因此絕大多數(shù)資料總是以下面的方式來說明JavaScript的面向?qū)ο笙到y(tǒng)中 的“封裝權(quán)限級別”問題: //--------------------------------------------------------- // JavaScript中OOP封裝性 //--------------------------------------------------------- function MyObject() { // 1. 私有成員和方法 var private_prop = 0; var private_method_1 = function() { // ... return 1 } function private_method_2() { // ... return 1 }
// 2. 特權(quán)方法 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)”表明只有在(構(gòu)造)函數(shù)內(nèi)部可訪問,,而“特權(quán)(privileged)” 是特指一種存取“私有域”的“公開(public)”方法。“公開(public)”表明在(構(gòu)造)函 數(shù)外可以調(diào)用和存取,。
除了上述的封裝權(quán)限之外,,一些文檔還介紹了其它兩種相關(guān)的概念: - 原型屬性:Classname.prototype.propertyName = someValue - (類)靜態(tài)屬性:Classname.propertyName = someValue
然而,從面向?qū)ο蟮慕嵌壬蟻碇v,,上面這些概念都很難自圓其說:JavaScript究竟是為何,、 以及如何劃分出這些封裝權(quán)限和概念來的呢?
——因為我們必須注意到下面這個例子所帶來的問題: //--------------------------------------------------------- // 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()的方式 來調(diào)用setValue/getValue,使得在MyFoo()調(diào)用的過程中替換this為obj1/obj2實例,。
然而我們發(fā)現(xiàn)“測試二”完成之后,,obj2、obj1以及function MyFoo()所持有的局部 變量都返回了“obj2”,?!@表明三個函數(shù)使用了同一個局部變量。
由此可見,,JavaScript在處理局部變量時,,對“普通函數(shù)”與“構(gòu)造器”是分別對待 的。這種處理策略在一些JavaScript相關(guān)的資料中被解釋作“面向?qū)ο笾械乃接杏?#8221; 問題,。而事實上,,我更愿意從源代碼一級來告訴你真相:這是對象的上下文環(huán)境的問 題?!徊贿^從表面看去,,“上下文環(huán)境”的問題被轉(zhuǎn)嫁到對象的封裝性問題上了,。
(在閱讀下面的文字之前,)先做一個概念性的說明: - 在普通函數(shù)中,,上下文環(huán)境被window對象所持有 - 在“構(gòu)造器和對象方法”中,,上下文環(huán)境被對象實例所持有
在JavaScript的實現(xiàn)代碼中,每次創(chuàng)建一個對象,,解釋器將為對象創(chuàng)建一個上下文環(huán)境 鏈,,用于存放對象在進(jìn)入“構(gòu)造器和對象方法”時對function()內(nèi)部數(shù)據(jù)的一個備份。 JavaScript保證這個對象在以后再進(jìn)入“構(gòu)造器和對象方法”內(nèi)部時,,總是持有該上下 文環(huán)境,,和一個與之相關(guān)的this對象。由于對象可能有多個方法,,且每個方法可能又存 在多層嵌套函數(shù),,因此這事實上構(gòu)成了一個上下文環(huán)境的樹型鏈表結(jié)構(gòu)。而在構(gòu)造器和 對象方法之外,,JavaScript不提供任何訪問(該構(gòu)造器和對象方法的)上下文環(huán)境的方法,。
簡而言之: - 上下文環(huán)境與對象實例調(diào)用“構(gòu)造器和對象方法”時相關(guān),而與(普通)函數(shù)無關(guān) - 上下文環(huán)境記錄一個對象在“構(gòu)造函數(shù)和對象方法”內(nèi)部的私有數(shù)據(jù) - 上下文環(huán)境采用鏈?zhǔn)浇Y(jié)構(gòu),,以記錄多層的嵌套函數(shù)中的上下文
由于上下文環(huán)境只與構(gòu)造函數(shù)及其內(nèi)部的嵌套函數(shù)有關(guān),,重新閱讀前面的代碼: //--------------------------------------------------------- // 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‘);
我們發(fā)現(xiàn)setValue()的確可以訪問到位于MyFoo()函數(shù)內(nèi)部的“局部變量i”,但是由于 setValue()方法的執(zhí)有者是MyFoo對象(記住函數(shù)也是對象),,因此MyFoo對象擁有MyFoo() 函數(shù)的唯一一份“上下文環(huán)境”,。
接下來MyFoo.setValue.call()調(diào)用雖然為setValue()傳入了新的this對象,但實際上 擁有“上下文環(huán)境”的仍舊是MyFoo對象,。因此我們看到無論創(chuàng)建多少個obj1/obj2,,最 終操作的都是同一個私有變量i。
全局函數(shù)/變量的“上下文環(huán)境”持有者為window,,因此下面的代碼說明了“為什么全 局變量能被任意的對象和函數(shù)訪問”: //--------------------------------------------------------- // 全局函數(shù)的上下文 //--------------------------------------------------------- /* 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,。接下來的推論是, 上下文環(huán)境決定了變量的“全局”與“私有”,。而不是反過來通過變量的私有與全局來 討論上下文環(huán)境問題,。
更進(jìn)一步的推論是:JavaScript中的全局變量與函數(shù),本質(zhì)上是window對象的私有變量 與方法,。而這個上下文環(huán)境塊,,位于所有(window對象內(nèi)部的)對象實例的上下文環(huán)境鏈 表的頂端,因此都可能訪問到,。
用“上下文環(huán)境”的理論,,你可以順利地解釋在本小節(jié)中,有關(guān)變量的“全局/局部” 作用域的問題,以及有關(guān)對象方法的封裝權(quán)限問題,。事實上,,在實現(xiàn)JavaScript的C源 代碼中,這個“上下文環(huán)境”被叫做“JSContext”,,并作為函數(shù)/方法的第一個參數(shù) 傳入,?!绻阌信d趣,,你可以從源代碼中證實本小節(jié)所述的理論。
另外,,《JavaScript權(quán)威指南》這本書中第4.7節(jié)也講述了這個問題,,但被叫做“變量 的作用域”。然而重要的是,,這本書把問題講反了,。——作者試圖用“全局,、局部的作 用域”,,來解釋產(chǎn)生這種現(xiàn)象的“上下文環(huán)境”的問題。因此這個小節(jié)顯得凌亂而且難 以自圓其說,。
不過在4.6.3小節(jié),,作者也提到了執(zhí)行環(huán)境(execution context)的問題,這就與我們這 里說的“上下文環(huán)境”是一致的了,。然而更麻煩的是,,作者又將讀者引錯了方法,試圖 用函數(shù)的上下文環(huán)境去解釋DOM和ScriptEngine中的問題,。
但這本書在“上下文環(huán)境鏈表”的查詢方式上的講述,,是正確的而合理的。只是把這個 叫成“作用域”有點不對,,或者不妥,。
~~~~~~~~~~~~~~~~~~ (續(xù))
7. JavaScript面向?qū)ο蟮闹С值难a(bǔ)充內(nèi)容 -------- 1). 類型系統(tǒng) ======================== 我們前面已經(jīng)完整地描述過JavaScript的兩種類型系統(tǒng)。包括: - 基礎(chǔ)類型系統(tǒng):由typeof()返回值的六種基礎(chǔ)類型 - 對象類型系統(tǒng):由new()返回值的,、構(gòu)造器和原型繼承組織起來的類型系統(tǒng)
JavaScript是弱類型語言,,因此類型自動轉(zhuǎn)換是它語言特性的一個重要組成部分。但 對于一個指定的變量而言,,(在某一時刻,,)它總是有確定的數(shù)據(jù)類型的。“運算”是 導(dǎo)致類型轉(zhuǎn)換的方法(但不是根源),,因此“運算結(jié)果的類型”的確定就非常重要,。關(guān) 于這一部分的內(nèi)容,推薦大家閱讀一份資料: http:///faq/faq_notes/type_convert.html
類型系統(tǒng)中還有一個特殊的組成部分,,就是“直接量”聲明,。下面的代碼簡述各種直 接量聲明的方法,,但不再詳述具體細(xì)節(jié): //--------------------------------------------------------- // 各種直接量聲明(一些錯誤格式或特例請查看JScript手冊) //--------------------------------------------------------- // 1. Number var n1 = 11; // 普通十進(jìn)制數(shù) var n2 = 013; // 八進(jìn)制數(shù) var n3 = 0xB; // 十六進(jìn)制數(shù) var n4 = 1.2; // 浮點值 var n5 = .2; // 浮點值 var n6 = 1.0e-4; // (或1e-4)浮點值
// 2. String var s1 = ‘test‘; // (或"test")字符串 var s2 = "test\n";// 帶轉(zhuǎn)義符的字符串(轉(zhuǎn)義符規(guī)則參見手冊) var s3 = "‘test‘";// 用""、‘‘以在字符串中使用引號 var s4 = "\xD"; // 用轉(zhuǎn)義符來聲明不可鍵入的字符
// 3. Boolean var b1 = true; var b2 = false;
// 4. Function function foo1() {}; // 利用編譯器特性直接聲明 var foo2 = function() {}; // 聲明匿名函數(shù)
// 5. Object // * 請留意聲明中對分隔符“,”的使用 var obj1 = null; // 空對象是可以被直接聲明的 var obj2 = { value1 : ‘value‘, // 對象屬性 foo1 : function() {}, // 利用匿名函數(shù)來直接聲明對象方法 foo2 : foo2 // 使方法指向已聲明過的函數(shù) }
// 6. RegExp var r1 = /^[O|o]n/; // 使用一對"/../"表達(dá)的即是正則表達(dá)式 var r2 = /^./gim; // (注意,) gim為正則表達(dá)式的三個參數(shù)
// 7. Array var arr1 = [1,,,1]; // 直接聲明, 包括一些"未定義(undefined)"值 var arr2 = [1,[1,‘a‘]]; // 異質(zhì)(非單一類型)的數(shù)組聲明 var arr3 = [[1],[2]]; // 多維數(shù)組(其實是從上一個概念衍生下來的
// 8. undefined var u1 = undefined; // 可以直接聲明, 這里的undefined是Global的屬性
有些時候,,我們可以“即聲明即使用”一個直接量,,下面的代碼演示這一特性: //--------------------------------------------------------- // 直接量的“即聲明即使用” //--------------------------------------------------------- var obj = function () { // 1. 聲明了一個匿名函數(shù) return { // 2. 函數(shù)執(zhí)行的結(jié)果是返回一個直接聲明的"對象" value: ‘test‘, method: function(){} } }(); // 3. 使匿名函數(shù)執(zhí)行并返回結(jié)果,以完成obj變量的聲明
在這個例子中,,很多處用到了直接量的聲明,。這其中函數(shù)直接聲明(并可以立即執(zhí)行)的特 性很有價值,例如在一個.js文件中試圖執(zhí)行一些代碼,,但不希望這些代碼中的變量聲明對 全局代碼導(dǎo)致影響,,因此可以在外層包裝一個匿名函數(shù)并使之執(zhí)行,例如: //--------------------------------------------------------- // 匿名函數(shù)的執(zhí)行 // (注:void用于使后面的函數(shù)會被執(zhí)行, 否則解釋器會認(rèn)為僅是聲明函數(shù)) //--------------------------------------------------------- void function() { if (isIE()) { // do something... } }();
2). 對象系統(tǒng) ======================== 對象系統(tǒng)中一個未被提及的重要內(nèi)容是delete運算,。它用于刪除數(shù)組元素,、對象屬性和已 聲明的變量。
由于delete運算不能刪除用var來聲明的變量,,也就意味著它只能刪除在函數(shù)內(nèi)/外聲明 的全局變量,。——這個說法有點別扭,,但事實上的確如此,。那么我們可以更深層地透視一 個真想:delete運算刪除變量的實質(zhì),是刪除用戶在window對象的上下文環(huán)境中聲明的屬 性,。
回到前面有關(guān)“上下文環(huán)境”的討論,,我們注意到(在函數(shù)外)聲明全局變量的三種形式: ---------- var global_1 = ‘全局變量1‘; global_2 = ‘全局變量2‘;
function foo() { global_3 = ‘全局變量3‘; } ----------
全局變量2和3都是“不用var聲明的變量”,這其實是在window對象的上下文環(huán)境中的 屬性聲明,。也就是說可以用window.global_2和window.global_3來存取它們,。這三種聲 明window對象的屬性的方法,與直接指定“window.global_value = <值>”這種方法的 唯一區(qū)別,,是在“for .. in”運算時,,這三種方法聲明的屬性/方法都會被隱藏。如下 例所示: //--------------------------------------------------------- // 全局變量上下文環(huán)境的一些特點:屬性名隱藏 //--------------------------------------------------------- var global_1 = ‘全局變量1‘; global_2 = ‘全局變量2‘;
void function foo() { global_3 = ‘全局變量3‘; }();
window.global_4 = ‘全局變量4‘;
for (var i in window) { document.writeln(i, ‘<br>‘); } document.writeln(‘<HR>‘); document.writeln(window.global_1, ‘<BR>‘); document.writeln(window.global_2, ‘<BR>‘); document.writeln(window.global_3, ‘<BR>‘);
我們注意到在返回的結(jié)果中不會出現(xiàn)全局變量1/2/3的屬性名,。但使用window.xxxx這種方 式仍可以存取到它們,。
在window上下文環(huán)境中,global_1實質(zhì)是該上下文中的私有變量,,我們在其它代碼中能存 取到它,,只是因為其它(所有的)代碼都在該上下文之內(nèi)。global_2/3則被(隱含地)聲明成 window的屬性,,而global_4則顯式地聲明為window的屬性,。
因此我們回到前面的結(jié)論: - 刪除(不用var聲明的)變量的實質(zhì),是刪除window對象的屬性。
此外,,我們也得到另外三條推論(最重要的是第一條): - delete能刪除數(shù)組元素,,實質(zhì)上是因為數(shù)組下標(biāo)也是數(shù)組對象的隱含屬性。 - 在復(fù)雜的系統(tǒng)中,,為減少變量名沖突,,應(yīng)盡量避免全局變量(和聲明)的使用,或采用 delete運算來清理window對象的屬性,。 - window對象是唯一可以讓用戶聲明“隱含的屬性”的對象,。——注意這只是表面的現(xiàn) 象,,因為事實上這只是JavaScript規(guī)范帶來的一個“附加效果”,。:)
delete清除window對象、系統(tǒng)對象,、用戶對象等的“用戶聲明屬性”,但不能清除如prototype,、 constructor這樣的系統(tǒng)屬性,。此外,delete也可以清除數(shù)組中的元素(但不會因為清除元 素而使數(shù)組長度發(fā)生變化),。例如: //--------------------------------------------------------- // delete運算的一些示例 //--------------------------------------------------------- var arr = [1, 2, 3]; var obj = {v1:1, v2:2}; global_variant = 3;
delete arr[2]; document.writeln(‘1‘ in arr, ‘<BR>‘); // 數(shù)組下標(biāo)事實上也是數(shù)組對象的隱含屬性 document.writeln(arr.length, ‘<BR>‘); // 數(shù)組長度不會因delete而改變
delete obj.v2; document.writeln(‘v2‘ in obj, ‘<BR>‘);
document.writeln(‘global_variant‘ in window, ‘<BR>‘); delete global_variant;
// 以下的代碼不能正常執(zhí)行,,這是IE的一個bug if (‘global_variant‘ in window) { document.writeln(‘bug test:‘, global_variant, ‘<BR>‘); }
最后這行代碼錯誤的根源,在于IE錯誤地檢測了‘global_variant‘在window的對象屬性 中是否仍然存在,。因為在同樣的位置,,“(‘global_variant‘ in window)”表達(dá)式的返 回結(jié)果居然為true!——firefox中沒有這個bug,。
delete清除掉屬性或數(shù)組元素,,并不表明腳本引擎會對于該屬性/元素執(zhí)行析構(gòu)。對象 的析構(gòu)操作是不確定的,,關(guān)于這一點請查看更前面的內(nèi)容,。
4. 實例和實例引用 -------- 在.NET Framework對CTS(Common Type System)約定“一切都是對象”,并分為“值 類型”和“引用類型”兩種,。其中“值類型”的對象在轉(zhuǎn)換成“引用類型”數(shù)據(jù)的 過程中,,需要進(jìn)行一個“裝箱”和“拆箱”的過程。
在JavaScript也有同樣的問題,。我們看到的typeof關(guān)鍵字,,返回以下六種數(shù)據(jù)類型: "number"、"string",、"boolean",、"object"、"function" 和 "undefined"。
我們也發(fā)現(xiàn)JavaScript的對象系統(tǒng)中,,有String,、Number、Function,、Boolean這四 種對象構(gòu)造器,。那么,我們的問題是:如果有一個數(shù)字A,,typeof(A)的結(jié)果,,到底會 是‘number‘呢,還是一個構(gòu)造器指向function Number()的對象呢,?
//--------------------------------------------------------- // 關(guān)于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]);
測試代碼的執(zhí)行結(jié)果如下: ----------- A1 is Value, type of number A2 is Object, construct by function Number() { [native code] } true,true -----------
我們注意到,,A1和A2的構(gòu)造器都指向Number。這意味著通過constructor屬性來識別 對象,,(有時)比typeof更加有效,。因為“值類型數(shù)據(jù)”A1作為一個對象來看待時, 與A2有完全相同的特性,。
——除了與實例引用有關(guān)的問題,。
參考JScript手冊,我們對其它基礎(chǔ)類型和構(gòu)造器做相同考察,,可以發(fā)現(xiàn): - 基礎(chǔ)類型中的undefined,、number、boolean和string,,是“值類型”變量 - 基礎(chǔ)類型中的array,、function和object,是“引用類型”變量 - 使用new()方法構(gòu)造出對象,,是“引用類型”變量
下面的代碼說明“值類型”與“引用類型”之間的區(qū)別: //--------------------------------------------------------- // 關(guān)于JavaScript類型系統(tǒng)中的值/引用問題 //--------------------------------------------------------- var str1 = ‘a(chǎn)bcdefgh‘, str2 = ‘a(chǎn)bcdefgh‘; var obj1 = new String(‘a(chǎn)bcdefgh‘), obj2 = new String(‘a(chǎn)bcdefgh‘);
document.writeln([str1==str2, str1===str2], ‘<br>‘); document.writeln([obj1==obj2, obj1===obj2]);
測試代碼的執(zhí)行結(jié)果如下: ----------- true, true false, false -----------
我們看到,,無論是等值運算(==),還是全等運算(===),,對“對象”和“值”的 理解都是不一樣的,。
更進(jìn)一步的理解這種現(xiàn)象,我們知道: - 運算結(jié)果為值類型,,或變量為值類型時,,等值(或全等)比較可以得到預(yù)想結(jié)果 - (即使包含相同的數(shù)據(jù),)不同的對象實例之間是不等值(或全等)的 - 同一個對象的不同引用之間,,是等值(==)且全等(===)的
但對于String類型,,有一點補(bǔ)充:根據(jù)JScript的描述,兩個字符串比較時,,只要有 一個是值類型,,則按值比較,。這意味著在上面的例子中,代碼“str1==obj1”會得到 結(jié)果true,。而全等(===)運算需要檢測變量類型的一致性,,因此“str1===obj1”的結(jié) 果返回false。
JavaScript中的函數(shù)參數(shù)總是傳入值參,,引用類型(的實例)是作為指針值傳入的,。因此 函數(shù)可以隨意重寫入口變量,而不用擔(dān)心外部變量被修改,。但是,,需要留意傳入的引用 類型的變量,因為對它方法調(diào)用和屬性讀寫可能會影響到實例本身,?!部梢酝?br>過引用類型的參數(shù)來傳出數(shù)據(jù),。
最后補(bǔ)充說明一下,,值類型比較會逐字節(jié)檢測對象實例中的數(shù)據(jù),效率低但準(zhǔn)確性高,; 而引用類型只檢測實例指針和數(shù)據(jù)類型,,因此效率高而準(zhǔn)確性低。如果你需要檢測兩個 引用類型是否真的包含相同的數(shù)據(jù),,可能你需要嘗試把它轉(zhuǎn)換成“字符串值”再來比較。
6. 函數(shù)的上下文環(huán)境 -------- 只要寫過代碼,,你應(yīng)該知道變量是有“全局變量”和“局部變量”之分的,。絕大多數(shù)的 JavaScript程序員也知道下面這些概念: //--------------------------------------------------------- // JavaScript中的全局變量與局部變量 //--------------------------------------------------------- var v1 = ‘全局變量-1‘; v2 = ‘全局變量-2‘;
function foo() { v3 = ‘全局變量-3‘;
var v4 = ‘只有在函數(shù)內(nèi)部并使用var定義的,才是局部變量‘; }
按照通常對語言的理解來說,,不同的代碼調(diào)用函數(shù),,都會擁有一套獨立的局部變量。 因此下面這段代碼很容易理解: //--------------------------------------------------------- // 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());
結(jié)果顯示false,,表明不同(實例的方法)調(diào)用返回的局部變量“obj1/obj2”是不相同,。
變量的局部、全局特性與OOP的封裝性中的“私有(private)”,、“公開(public)”具 有類同性,。因此絕大多數(shù)資料總是以下面的方式來說明JavaScript的面向?qū)ο笙到y(tǒng)中 的“封裝權(quán)限級別”問題: //--------------------------------------------------------- // JavaScript中OOP封裝性 //--------------------------------------------------------- function MyObject() { // 1. 私有成員和方法 var private_prop = 0; var private_method_1 = function() { // ... return 1 } function private_method_2() { // ... return 1 }
// 2. 特權(quán)方法 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)”表明只有在(構(gòu)造)函數(shù)內(nèi)部可訪問,,而“特權(quán)(privileged)” 是特指一種存取“私有域”的“公開(public)”方法,。“公開(public)”表明在(構(gòu)造)函 數(shù)外可以調(diào)用和存取。
除了上述的封裝權(quán)限之外,,一些文檔還介紹了其它兩種相關(guān)的概念: - 原型屬性:Classname.prototype.propertyName = someValue - (類)靜態(tài)屬性:Classname.propertyName = someValue
然而,,從面向?qū)ο蟮慕嵌壬蟻碇v,,上面這些概念都很難自圓其說:JavaScript究竟是為何、 以及如何劃分出這些封裝權(quán)限和概念來的呢,?
——因為我們必須注意到下面這個例子所帶來的問題: //--------------------------------------------------------- // 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()的方式 來調(diào)用setValue/getValue,,使得在MyFoo()調(diào)用的過程中替換this為obj1/obj2實例,。
然而我們發(fā)現(xiàn)“測試二”完成之后,obj2,、obj1以及function MyFoo()所持有的局部 變量都返回了“obj2”,。——這表明三個函數(shù)使用了同一個局部變量,。
由此可見,,JavaScript在處理局部變量時,對“普通函數(shù)”與“構(gòu)造器”是分別對待 的,。這種處理策略在一些JavaScript相關(guān)的資料中被解釋作“面向?qū)ο笾械乃接杏?#8221; 問題,。而事實上,我更愿意從源代碼一級來告訴你真相:這是對象的上下文環(huán)境的問 題,?!徊贿^從表面看去,“上下文環(huán)境”的問題被轉(zhuǎn)嫁到對象的封裝性問題上了,。
(在閱讀下面的文字之前,,)先做一個概念性的說明: - 在普通函數(shù)中,上下文環(huán)境被window對象所持有 - 在“構(gòu)造器和對象方法”中,,上下文環(huán)境被對象實例所持有
在JavaScript的實現(xiàn)代碼中,,每次創(chuàng)建一個對象,解釋器將為對象創(chuàng)建一個上下文環(huán)境 鏈,,用于存放對象在進(jìn)入“構(gòu)造器和對象方法”時對function()內(nèi)部數(shù)據(jù)的一個備份,。 JavaScript保證這個對象在以后再進(jìn)入“構(gòu)造器和對象方法”內(nèi)部時,總是持有該上下 文環(huán)境,,和一個與之相關(guān)的this對象,。由于對象可能有多個方法,且每個方法可能又存 在多層嵌套函數(shù),,因此這事實上構(gòu)成了一個上下文環(huán)境的樹型鏈表結(jié)構(gòu),。而在構(gòu)造器和 對象方法之外,JavaScript不提供任何訪問(該構(gòu)造器和對象方法的)上下文環(huán)境的方法,。
簡而言之: - 上下文環(huán)境與對象實例調(diào)用“構(gòu)造器和對象方法”時相關(guān),,而與(普通)函數(shù)無關(guān) - 上下文環(huán)境記錄一個對象在“構(gòu)造函數(shù)和對象方法”內(nèi)部的私有數(shù)據(jù) - 上下文環(huán)境采用鏈?zhǔn)浇Y(jié)構(gòu),以記錄多層的嵌套函數(shù)中的上下文
由于上下文環(huán)境只與構(gòu)造函數(shù)及其內(nèi)部的嵌套函數(shù)有關(guān),,重新閱讀前面的代碼: //--------------------------------------------------------- // 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‘);
我們發(fā)現(xiàn)setValue()的確可以訪問到位于MyFoo()函數(shù)內(nèi)部的“局部變量i”,,但是由于 setValue()方法的執(zhí)有者是MyFoo對象(記住函數(shù)也是對象),,因此MyFoo對象擁有MyFoo() 函數(shù)的唯一一份“上下文環(huán)境”。
接下來MyFoo.setValue.call()調(diào)用雖然為setValue()傳入了新的this對象,,但實際上 擁有“上下文環(huán)境”的仍舊是MyFoo對象,。因此我們看到無論創(chuàng)建多少個obj1/obj2,最 終操作的都是同一個私有變量i,。
全局函數(shù)/變量的“上下文環(huán)境”持有者為window,,因此下面的代碼說明了“為什么全 局變量能被任意的對象和函數(shù)訪問”: //--------------------------------------------------------- // 全局函數(shù)的上下文 //--------------------------------------------------------- /* 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。接下來的推論是,, 上下文環(huán)境決定了變量的“全局”與“私有”,。而不是反過來通過變量的私有與全局來 討論上下文環(huán)境問題。
更進(jìn)一步的推論是:JavaScript中的全局變量與函數(shù),,本質(zhì)上是window對象的私有變量 與方法,。而這個上下文環(huán)境塊,位于所有(window對象內(nèi)部的)對象實例的上下文環(huán)境鏈 表的頂端,,因此都可能訪問到,。
用“上下文環(huán)境”的理論,你可以順利地解釋在本小節(jié)中,,有關(guān)變量的“全局/局部” 作用域的問題,,以及有關(guān)對象方法的封裝權(quán)限問題。事實上,,在實現(xiàn)JavaScript的C源 代碼中,,這個“上下文環(huán)境”被叫做“JSContext”,并作為函數(shù)/方法的第一個參數(shù) 傳入,?!绻阌信d趣,你可以從源代碼中證實本小節(jié)所述的理論,。
另外,《JavaScript權(quán)威指南》這本書中第4.7節(jié)也講述了這個問題,,但被叫做“變量 的作用域”,。然而重要的是,這本書把問題講反了,?!髡咴噲D用“全局、局部的作 用域”,,來解釋產(chǎn)生這種現(xiàn)象的“上下文環(huán)境”的問題,。因此這個小節(jié)顯得凌亂而且難 以自圓其說。
不過在4.6.3小節(jié),,作者也提到了執(zhí)行環(huán)境(execution context)的問題,,這就與我們這 里說的“上下文環(huán)境”是一致的了,。然而更麻煩的是,作者又將讀者引錯了方法,,試圖 用函數(shù)的上下文環(huán)境去解釋DOM和ScriptEngine中的問題,。
但這本書在“上下文環(huán)境鏈表”的查詢方式上的講述,是正確的而合理的,。只是把這個 叫成“作用域”有點不對,,或者不妥。
|