??由于 JavaScript 本身是動態(tài)語言缸沃,而且多年來一直沒有固定的開發(fā)工具澄步,因此人們普遍認(rèn)為它是一種最難于調(diào)試的編程語言。
??腳本出錯時和泌,瀏覽器通常會給出類似于 "object expected"(缺少對象)這樣的消息村缸,沒有上下文信息,讓人摸不著頭腦武氓。
??ECMAScript 第 3 版致力于解決這個問題梯皿,專門引入了 try-catch 和 throw 語句以及一些錯誤類型,意在讓開發(fā)人員能夠適當(dāng)?shù)靥幚礤e誤县恕。
??幾年之后东羹,Web 瀏覽器中也出現(xiàn)了一些 JavaScript 調(diào)試程序和工具。2008 年以來忠烛,大多數(shù) Web 瀏覽器都已經(jīng)具備了一些調(diào)試 JavaScript 代碼的能力属提。
??有了語言特性和工具支持之后,現(xiàn)在的開發(fā)人員已經(jīng)能夠適當(dāng)?shù)貙崿F(xiàn)錯誤處理美尸,并且能夠找到錯誤的根源冤议。
1、瀏覽器報告的錯誤
??IE师坎、Firefox恕酸、Safari、Chrome 和 Opera 等主流瀏覽器胯陋,都具有某種向用戶報告 JavaScript 錯誤的機(jī)制蕊温。默認(rèn)情況下,所有瀏覽器都會隱藏此類信息遏乔,畢竟除了開發(fā)人員之外义矛,很少有人關(guān)心這些內(nèi)容。
??因此盟萨,在基于瀏覽器編寫 JavaScript 腳本時凉翻,別忘了啟用瀏覽器的 JavaScript 報告功能,以便及時收到錯誤通知鸯旁。
2噪矛、錯誤處理
??錯誤處理在程序設(shè)計中的重要性是勿庸置疑的量蕊。任何有影響力的 Web 應(yīng)用程序都需要一套完善的錯誤處理機(jī)制,當(dāng)然艇挨,大多數(shù)佼佼者確實做到了這一點(diǎn)残炮,但通常只有服務(wù)器端應(yīng)用程序才能做到如此。
??實際上缩滨,服務(wù)器端團(tuán)隊往往會在錯誤處理機(jī)制上投入較大的精力势就,通常要考慮按照類型、頻率脉漏,或者其他重要的標(biāo)準(zhǔn)對錯誤進(jìn)行分類苞冯。這樣一來,開發(fā)人員就能夠理解用戶在使用簡單數(shù)據(jù)庫查詢或者報告生成腳本時侧巨,應(yīng)用程序可能會出現(xiàn)的問題舅锄。
??雖然客戶端應(yīng)用程序的錯誤處理也同樣重要,但真正受到重視司忱,還是最近幾年的事皇忿。實際上,我們要面對這樣一個不爭的事實:使用 Web 的絕大多數(shù)人都不是技術(shù)高手坦仍,其中甚至有很多人根本就不明
白瀏覽器到底是什么鳍烁,更不用說讓他們說喜歡哪一個了。
??本章前面討論過繁扎,每個瀏覽器在發(fā)生 JavaScript 錯誤時的行為都或多或少有一些差異幔荒。有的會顯示小圖標(biāo),有的則什么動靜也沒有梳玫,瀏覽器對 JavaScript 錯誤的這些默認(rèn)行為對最終用戶而言爹梁,毫無規(guī)律可循。最理想的情況下汽纠,用戶遇到錯誤搞不清為什么卫键,他們會再試著重做一次傀履;最糟糕的情況下虱朵,用戶會惱羞成怒,一去不復(fù)返了钓账。
??良好的錯誤處理機(jī)制可以讓用戶及時得到提醒碴犬,知道到底發(fā)生了什么事,因而不會驚惶失措梆暮。為此服协,作為開發(fā)人員,我們必須理解在處理 JavaScript 錯誤的時候啦粹,都有哪些手段和工具可以利用偿荷。
2.1窘游、try-catch 語句
??ECMA-262 第 3 版引入了 try-catch 語句,作為 JavaScript 中處理異常的一種標(biāo)準(zhǔn)方式跳纳∪淌危基本的語法如下所示,顯而易見寺庄,這與 Java 中的 try-catch 語句是完全相同的艾蓝。
try {
// 可能會導(dǎo)致錯誤的代碼
} catch(error) {
// 在錯誤發(fā)生時怎么處理
}
??也就是說,我們應(yīng)該把所有可能會拋出錯誤的代碼都放在 try 語句塊中斗塘,而把那些用于錯誤處理的代碼放在 catch 塊中赢织。例如:
try {
window.someNodexistentFunction();
} catch (error) {
alert("An error happened!");
}
??如果 try 塊中的任何代碼發(fā)生了錯誤,就會立即退出代碼執(zhí)行過程馍盟,然后接著執(zhí)行 catch 塊于置。此時,catch 塊會接收到一個包含錯誤信息的對象贞岭。
??與在其他語言中不同的是俱两,即使你不想使用這個錯誤對象,也要給它起個名字曹步。
??這個對象中包含的實際信息會因瀏覽器而異宪彩,但共同的是有一個保存著錯誤消息的 message 屬性。
??ECMA-262 還規(guī)定了一個保存錯誤類型的 name 屬性讲婚;當(dāng)前所有瀏覽器都支持這個屬性(Opera 9 之前的版本不支持這個屬性)尿孔。因此,在發(fā)生錯誤時筹麸,就可以像下面這樣實事求是地顯示瀏覽器給出的消息活合。
try {
window.someNodexistenFunction();
} catch (error) {
alert(error.message);
}
??上述例子在向用戶顯示錯誤消息時,使用了錯誤對象的 message 屬性物赶。這個 message 屬性是唯一一個能夠保證所有瀏覽器都支持的屬性白指,除此之外,IE酵紫、Firefox告嘲、Safari、Chrome 以及 Opera 都為事件對象添加了其他相關(guān)信息奖地。
??IE 添加了與 message 屬性完全相同的 description 屬性橄唬,還添加了保存著內(nèi)部錯誤數(shù)量的 number 屬性。
??Firefox 添加了 fileName参歹、lineNumber 和 stack(包含棧跟蹤信息)屬性仰楚。
??Safari 添加了line(表示行號)、sourceId(表示內(nèi)部錯誤代碼) 和 sourceURL 屬性。
??當(dāng)然僧界,在跨瀏覽器編程時侨嘀,最好還是只使用 message 屬性。
1. finally 子句
??雖然在 try-catch 語句中是可選的捂襟,但 finally 子句一經(jīng)使用飒炎,其代碼無論如何都會執(zhí)行。
??換句話說笆豁,try 語句塊中的代碼全部正常執(zhí)行郎汪,finally 子句會執(zhí)行;如果因為出錯而執(zhí)行了 catch 語句塊闯狱,finally 子句照樣還會執(zhí)行煞赢。
??只要代碼中包含 finally 子句,則無論 try 或 catch 語句塊中包含什么代碼 —— 甚至 return 語句哄孤,都不會阻止 finally 子句的執(zhí)行照筑。示例:
function testFinally () {
try {
return 2;
} catch (error) {
return 1;
} finally {
return 0;
}
}
??上述函數(shù)在 try-catch 語句的每一部分都放了一條 return 語句。表面上看瘦陈,調(diào)用這個函數(shù)會返回 2,因為返回 2 的 return 語句位于 try 語句塊中凝危,而執(zhí)行該語句又不會出錯。
??可是晨逝,由于最后還有一個 finally 子句蛾默,結(jié)果就會導(dǎo)致該 return 語句被忽略;也就是說捉貌,調(diào)用這個函數(shù)只能返回 0支鸡。如果把 finally 子句拿掉,這個函數(shù)將返回 2趁窃。
??如果提供 finally 子句牧挣,則 catch 子句就成了可選的(catch 或 finally 有一個即可)。IE7 及更早版本中有一個 bug:除非有 catch 語句醒陆,否則 finally 中的代碼永遠(yuǎn)不會執(zhí)行瀑构。如果你仍然要考慮 IE 的早期版本,那就只好提供一個 catch 子句刨摩,哪怕里面什么都不寫寺晌。IE8 修復(fù)了這個 bug。
??請務(wù)必記住码邻,只要代碼中包含 finally 子句折剃,那么無論 try 還是 catch 語句塊中的 return 語句都將被忽略。因此像屋,在使用 finally 子句之前,一定要非常清楚你想讓代碼怎么樣边篮。
- 錯誤類型
??執(zhí)行代碼期間可能會發(fā)生的錯誤有多種類型己莺。每種錯誤都有對應(yīng)的錯誤類型奏甫,而當(dāng)錯誤發(fā)生時,就會拋出相應(yīng)類型的錯誤對象凌受。ECMA-262 定義了下列 7 種錯誤類型:
- Error
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError
??其中阵子,Error 是基類型,其他錯誤類型都繼承自該類型胜蛉。因此挠进,所有錯誤類型共享了一組相同的屬性(錯誤對象中的方法全是默認(rèn)的對象方法)。
??Error 類型的錯誤很少見誊册,如果有也是瀏覽器拋出的领突;這個基類型的主要目的是供開發(fā)人員拋出自定義錯誤。
??EvalError 類型的錯誤會在使用 eval() 函數(shù)而發(fā)生異常時被拋出案怯。ECMA-262 中對這個錯誤有如下描述:“如果以非直接調(diào)用的方式使用 eval 屬性的值(換句話說君旦,沒有明確地將其名稱作為一個 Identifier,即用作 CallExpression 中的 MemberExpression)嘲碱,或者為 eval 屬性賦值金砍。”簡單地說麦锯,如果沒有把 eval() 當(dāng)成函數(shù)調(diào)用恕稠,就會拋出錯誤,例如:
new eval(); // 拋出 EvalError
eval = foo; // 拋出 EvalError
??在實踐中扶欣,瀏覽器不一定會在應(yīng)該拋出錯誤時就拋出 EvalError谱俭。例如,F(xiàn)irefox 4+ 和 IE8 對第一種情況會拋出 TypeError宵蛀,而第二種情況會成功執(zhí)行昆著,不發(fā)生錯誤。有鑒于此术陶,加上在實際開發(fā)中極少會這樣使用 eval()凑懂,所以遇到這種錯誤類型的可能性極小。
??RangeError 類型的錯誤會在數(shù)值超出相應(yīng)范圍時觸發(fā)梧宫。例如接谨,在定義數(shù)組時,如果指定了數(shù)組不支持的項數(shù)(如 -20 或 Number.MAX_VALUE)塘匣,就會觸發(fā)這種錯誤脓豪。下面是具體的例子。
var items1 = new Array(-20); // 拋出 RangeError
var items2 = new Array(Number.MAX_VALUE); // 拋出 RangeError
??JavaScript 中經(jīng)常會出現(xiàn)這種范圍錯誤忌卤。
??在找不到對象的情況下扫夜,會發(fā)生 ReferenceError(這種情況下,會直接導(dǎo)致人所共知的"object expected"瀏覽器錯誤)。通常笤闯,在訪問不存在的變量時堕阔,就會發(fā)生這種錯誤,例如:
var obj = x; // 在 x 并未聲明的情況下拋出 ReferenceError
??至于 SyntaxError颗味,當(dāng)我們把語法錯誤的 JavaScript 字符串傳入 eval() 函數(shù)時超陆,就會導(dǎo)致此類錯誤。例如:
eval("a ++ b"); // 拋出 SyntaxError
??如果語法錯誤的代碼出現(xiàn)在 eval() 函數(shù)之外浦马,則不太可能使用 SyntaxError时呀,因為此時的語法錯誤會導(dǎo)致 JavaScript 代碼立即停止執(zhí)行。
??TypeError 類型在 JavaScript 中會經(jīng)常用到晶默,在變量中保存著意外的類型時谨娜,或者在訪問不存在的方法時,都會導(dǎo)致這種錯誤荤胁。錯誤的原因雖然多種多樣瞧预,但歸根結(jié)底還是由于在執(zhí)行特定于類型的操作時,變量的類型并不符合要求所致仅政。下面來看幾個例子垢油。
var o = new 10; // 拋出 TypeError
alert("name" in true); // 拋出 TypeError
Function.prototype.toString.call("name"); // 拋出 TypeError
??最常發(fā)生類型錯誤的情況,就是傳遞給函數(shù)的參數(shù)事先未經(jīng)檢查圆丹,結(jié)果傳入類型與預(yù)期類型不相符滩愁。
??在使用 encodeURI() 或 decodeURI(),而 URI 格式不正確時辫封,就會導(dǎo)致 URIError 錯誤硝枉。這種錯誤也很少見,因為前面說的這兩個函數(shù)的容錯性非常高倦微。
??利用不同的錯誤類型妻味,可以獲悉更多有關(guān)異常的信息,從而有助于對錯誤作出恰當(dāng)?shù)奶幚硇栏!R胫厘e誤的類型责球,可以像下面這樣在 try-catch 語句的 catch 語句中使用 instanceof 操作符。
try {
someFunction();
} catch (error){
if (error instanceof TypeError){
// 處理類型錯誤
} else if (error instanceof ReferenceError){
// 處理引用錯誤
} else {
// 處理其他類型的錯誤
}
}
??在跨瀏覽器編程中拓劝,檢查錯誤類型是確定處理方式的最簡便途徑雏逾;包含在 message 屬性中的錯誤消息會因瀏覽器而異。
3. 合理使用 try-catch
??當(dāng) try-catch 語句中發(fā)生錯誤時郑临,瀏覽器會認(rèn)為錯誤已經(jīng)被處理了栖博,因而不會通過本章前面討論的機(jī)制記錄或報告錯誤。對于那些不要求用戶懂技術(shù)厢洞,也不需要用戶理解錯誤的 Web 應(yīng)用程序仇让,這應(yīng)該說是個理想的結(jié)果典奉。
??不過,try-catch 能夠讓我們實現(xiàn)自己的錯誤處理機(jī)制妹孙。使用 try-catch 最適合處理那些我們無法控制的錯誤秋柄。假設(shè)你在使用一個大型 JavaScript 庫中的函數(shù)获枝,該函數(shù)可能會有意無意地拋出一些錯誤蠢正。由于我們不能修改這個庫的源代碼,所以大可將對該函數(shù)的調(diào)用放在 try-catch 語句當(dāng)中省店,萬一有什么錯誤發(fā)生嚣崭,也好恰當(dāng)?shù)靥幚硭鼈儭?br>
??在明明白白地知道自己的代碼會發(fā)生錯誤時,再使用 try-catch 語句就不太合適了懦傍。例如雹舀,如果傳遞給函數(shù)的參數(shù)是字符串而非數(shù)值,就會造成函數(shù)出錯粗俱,那么就應(yīng)該先檢查參數(shù)的類型说榆,然后再決定如何去做。在這種情況下寸认,不應(yīng)用使用 try-catch 語句签财。
2.2、拋出錯誤
??與 try-catch 語句相配的還有一個 throw 操作符偏塞,用于隨時拋出自定義錯誤唱蒸。拋出錯誤時,必須要給 throw 操作符指定一個值灸叼,這個值是什么類型神汹,沒有要求。下列代碼都是有效的古今。
throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript"};
??在遇到 throw 操作符時屁魏,代碼會立即停止執(zhí)行。僅當(dāng)有 try-catch 語句捕獲到被拋出的值時捉腥,代碼才會繼續(xù)執(zhí)行氓拼。
??通過使用某種內(nèi)置錯誤類型,可以更真實地模擬瀏覽器錯誤但狭。每種錯誤類型的構(gòu)造函數(shù)接收一個參數(shù)披诗,即實際的錯誤消息。下面是一個例子立磁。
throw new Error("Something bad happened.");
??這行代碼拋出了一個通用錯誤呈队,帶有一條自定義錯誤消息。瀏覽器會像處理自己生成的錯誤一樣唱歧,來處理這行代碼拋出的錯誤宪摧。換句話說询枚,瀏覽器會以常規(guī)方式報告這一錯誤,并且會顯示這里的自定義錯誤消息击狮。像下面使用其他錯誤類型筹陵,也可以模擬出類似的瀏覽器錯誤。
throw new SyntaxError("I don’t like your syntax.");
throw new TypeError("What type of variable do you take me for?");
throw new RangeError("Sorry, you just don’t have the range.");
throw new EvalError("That doesn’t evaluate.");
throw new URIError("Uri, is that you?");
throw new ReferenceError("You didn’t cite your references properly.");
??在創(chuàng)建自定義錯誤消息時最常用的錯誤類型是 Error沿彭、RangeError朽砰、ReferenceError 和 TypeError。
??另外喉刘,利用原型鏈還可以通過繼承 Error 來創(chuàng)建自定義錯誤類型瞧柔。此時,需要為新創(chuàng)建的錯誤類型指定 name 和 message 屬性睦裳。來看一個例子造锅。
function CustomError(message){
this.name = "CustomError";
this.message = message;
}
CustomError.prototype = new Error();
throw new CustomError("My message");
??瀏覽器對待繼承自 Error 的自定義錯誤類型,就像對待其他錯誤類型一樣廉邑。如果要捕獲自己拋出的錯誤并且把它與瀏覽器錯誤區(qū)別對待的話哥蔚,創(chuàng)建自定義錯誤是很有用的。
??IE 只有在拋出 Error 對象的時候才會顯示自定義錯誤消息蛛蒙。對于其他類型糙箍,它都無一例外地顯示"exception thrown and not caught"(拋出了異常,且未被捕獲)宇驾。
1. 拋出錯誤的時機(jī)
??要針對函數(shù)為什么會執(zhí)行失敗給出更多信息倍靡,拋出自定義錯誤是一種很方便的方式。應(yīng)該在出現(xiàn)某種特定的已知錯誤條件课舍,導(dǎo)致函數(shù)無法正常執(zhí)行時拋出錯誤塌西。換句話說,瀏覽器會在某種特定的條件下執(zhí)行函數(shù)時拋出錯誤筝尾。例如捡需,下面的函數(shù)會在參數(shù)不是數(shù)組的情況下失敗。
function process(values){
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
??如果執(zhí)行這個函數(shù)時傳給它一個字符串參數(shù)筹淫,那么對 sort() 的調(diào)用就會失敗站辉。對此,不同瀏覽器會給出不同的錯誤消息损姜,但都不是特別明確饰剥,如下所示。
- IE:屬性或方法不存在摧阅。
- Firefox:values.sort() 不是函數(shù)汰蓉。
- Safari:值 undefined(表達(dá)式 values.sort 的結(jié)果)不是對象。
- Chrome:對象名沒有方法'sort'棒卷。
- Opera:類型不匹配(通常是在需要對象的地方使用了非對象值)顾孽。
??盡管 Firefox祝钢、Chrome 和 Safari 都明確指出了代碼中導(dǎo)致錯誤的部分,但錯誤消息并沒有清楚地告訴我們到底出了什么問題若厚,該怎么修復(fù)問題拦英。
??在處理類似前面例子中的那個函數(shù)時,通過調(diào)試處理這些錯誤消息沒有什么困難测秸。但是疤估,在面對包含數(shù)千行 JavaScript 代碼的復(fù)雜的 Web 應(yīng)用程序時,要想查找錯誤來源就沒有那么容易了乞封。這種情況下做裙,帶有適當(dāng)信息的自定義錯誤能夠顯著提升代碼的可維護(hù)性岗憋。來看下面的例子肃晚。
function process(values){
if (!(values instanceof Array)){
throw new Error("process(): Argument must be an array.");
}
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
??在重寫后的這個函數(shù)中,如果 values 參數(shù)不是數(shù)組仔戈,就會拋出一個錯誤关串。錯誤消息中包含了函數(shù)的名稱,以及為什么會發(fā)生錯誤的明確描述监徘。如果一個復(fù)雜的 Web 應(yīng)用程序發(fā)生了這個錯誤晋修,那么查找問題的根源也就容易多了。
??建議讀者在開發(fā) JavaScript 代碼的過程中凰盔,重點(diǎn)關(guān)注函數(shù)和可能導(dǎo)致函數(shù)執(zhí)行失敗的因素墓卦。良好的錯誤處理機(jī)制應(yīng)該可以確保代碼中只發(fā)生你自己拋出的錯誤。
2. 拋出錯誤與使用 try-catch
??關(guān)于何時該拋出錯誤户敬,而何時該使用 try-catch 來捕獲它們落剪,是一個老生常談的問題。一般來說尿庐,應(yīng)用程序架構(gòu)的較低層次中經(jīng)常會拋出錯誤忠怖,但這個層次并不會影響當(dāng)前執(zhí)行的代碼,因而錯誤通常得不到真正的處理抄瑟。
??如果你打算編寫一個要在很多應(yīng)用程序中使用的 JavaScript 庫凡泣,甚至只編寫一個可能會在應(yīng)用程序內(nèi)部多個地方使用的輔助函數(shù),我都強(qiáng)烈建議你在拋出錯誤時提供詳盡的信息皮假。然后鞋拟,即可在應(yīng)用程序中捕獲并適當(dāng)?shù)靥幚磉@些錯誤。
??說到拋出錯誤與捕獲錯誤惹资,我們認(rèn)為只應(yīng)該捕獲那些你確切地知道該如何處理的錯誤贺纲。捕獲錯誤的目的在于避免瀏覽器以默認(rèn)方式處理它們;而拋出錯誤的目的在于提供錯誤發(fā)生具體原因的消息布轿。
2.3哮笆、錯誤(error)事件
??任何沒有通過 try-catch 處理的錯誤都會觸發(fā) window 對象的 error 事件来颤。這個事件是 Web 瀏覽器最早支持的事件之一,IE稠肘、Firefox 和 Chrome 為保持向后兼容福铅,并沒有對這個事件作任何修改(Opera 和 Safari 不支持 error 事件)。
??在任何 Web 瀏覽器中项阴,onerror 事件處理程序都不會創(chuàng)建 event 對象滑黔,但它可以接收三個參數(shù):錯誤消息、錯誤所在的 URL 和行號环揽。
??多數(shù)情況下略荡,只有錯誤消息有用,因為 URL 只是給出了文檔的位置歉胶,而行號所指的代碼行既可能出自嵌入的 JavaScript 代碼汛兜,也可能出自外部的文件。
??要指定 onerror 事件處理程序通今,必須使用如下所示的 DOM0 級技術(shù)粥谬,它沒有遵循“DOM2 級事件”的標(biāo)準(zhǔn)格式。
window.onerror = function(message, url, line){
alert(message);
};
??只要發(fā)生錯誤辫塌,無論是不是瀏覽器生成的漏策,都會觸發(fā) error 事件,并執(zhí)行這個事件處理程序臼氨。然后掺喻,瀏覽器默認(rèn)的機(jī)制發(fā)揮作用,像往常一樣顯示出錯誤消息储矩。像下面這樣在事件處理程序中返回 false感耙,可以阻止瀏覽器報告錯誤的默認(rèn)行為。
window.onerror = function(message, url, line){
alert(message);
return false;
};
??通過返回 false椰苟,這個函數(shù)實際上就充當(dāng)了整個文檔中的 try-catch 語句抑月,可以捕獲所有無代碼處理的運(yùn)行時錯誤。這個事件處理程序是避免瀏覽器報告錯誤的最后一道防線舆蝴,理想情況下谦絮,只要可能就不應(yīng)該使用它。只要能夠適當(dāng)?shù)厥褂?try-catch 語句洁仗,就不會有錯誤交給瀏覽器层皱,也就不會觸發(fā) error 事件。
??瀏覽器在使用這個事件處理錯誤時的方式有明顯不同赠潦。在 IE 中叫胖,即使發(fā)生 error 事件,代碼仍然會正常執(zhí)行她奥;所有變量和數(shù)據(jù)都將得到保留瓮增,因此能在 onerror 事件處理程序中訪問它們怎棱。
??但在 Firefox 中,常規(guī)代碼會停止執(zhí)行绷跑,事件發(fā)生之前的所
有變量和數(shù)據(jù)都將被銷毀拳恋,因此幾乎就無法判斷錯誤了。
??圖像也支持 error 事件砸捏。只要圖像的 src 特性中的 URL 不能返回可以被識別的圖像格式谬运,就會觸發(fā) error 事件。此時的 error 事件遵循 DOM 格式垦藏,會返回一個以圖像為目標(biāo)的 event 對象梆暖。下面是一個例子。
var image = new Image();
EventUtil.addHandler(image, "load", function(event){
alert("Image loaded!");
});
EventUtil.addHandler(image, "error", function(event){
alert("Image not loaded!");
});
image.src = "smilex.gif"; // 指定不存在的文件
??在這個例子中掂骏,當(dāng)加載圖像失敗時就會顯示一個警告框轰驳。需要注意的是,發(fā)生 error 事件時芭挽,圖像下載過程已經(jīng)結(jié)束滑废,也就是說不能再重新下載了。
2.4袜爪、處理錯誤的策略
??過去,所謂 Web 應(yīng)用程序的錯誤處理策略僅限于服務(wù)器端薛闪。在談到錯誤與錯誤處理時辛馆,通常要考慮很多方面,涉及一些工具豁延,例如記錄和監(jiān)控系統(tǒng)昙篙。這些工具的用途在于分析錯誤模式,追查錯誤原因诱咏,同時幫助確定錯誤會影響到多少用戶苔可。
??在 Web 應(yīng)用程序的 JavaScript 這一端,錯誤處理策略也同樣重要袋狞。由于任何 JavaScript 錯誤都可能導(dǎo)致網(wǎng)頁無法使用焚辅,因此搞清楚何時以及為什么發(fā)生錯誤至關(guān)重要。絕大多數(shù) Web 應(yīng)用程序的用戶都不懂技術(shù)苟鸯,遇到錯誤時很容易心煩意亂同蜻。有時候,他們可能會刷新頁面以期解決問題早处,而有時候則會放棄努力湾蔓。作為開發(fā)人員,必須要知道代碼何時可能出錯砌梆,會出什么錯默责,同時還要有一個跟蹤此類問題的系統(tǒng)贬循。
2.5、常見的錯誤類型
??錯誤處理的核心桃序,是首先要知道代碼里會發(fā)生什么錯誤甘有。由于 JavaScript 是松散類型的,而且也不會驗證函數(shù)的參數(shù)葡缰,因此錯誤只會在代碼運(yùn)行期間出現(xiàn)亏掀。一般來說,需要關(guān)注三種錯誤:
- 類型轉(zhuǎn)換錯誤
- 數(shù)據(jù)類型錯誤
- 通信錯誤
??以上錯誤分別會在特定的模式下或者沒有對值進(jìn)行足夠的檢查的情況下發(fā)生泛释。
1. 類型轉(zhuǎn)換錯誤
??類型轉(zhuǎn)換錯誤發(fā)生在使用某個操作符滤愕,或者使用其他可能會自動轉(zhuǎn)換值的數(shù)據(jù)類型的語言結(jié)構(gòu)時。
??在使用相等(==)和不相等(!=)操作符怜校,或者在 if间影、for 及 while 等流控制語句中使用非布爾值時,最常發(fā)生類型轉(zhuǎn)換錯誤茄茁。
??相等和不相等操作符在執(zhí)行比較之前會先轉(zhuǎn)換不同類型的值魂贬。由于在非動態(tài)語言中付燥,開發(fā)人員都使用相同的符號執(zhí)行直觀的比較愈犹,因此在 JavaScript 中往往也會以相同方式錯誤地使用它們键科。
??多數(shù)情況下勋颖,我們建議使用全等(===)和不全等(!==)操作符,以避免類型轉(zhuǎn)換勋锤。來看一個例子。
alert(5 == "5"); // true
alert(5 === "5"); // false
alert(1 == true); // true
alert(1 === true); // false
??這里使用了相等和全等操作符比較了數(shù)值 5 和字符串"5"茄厘。相等操作符首先會將數(shù)值 5 轉(zhuǎn)換成字符串"5"蚕断,然后再將其與另一個字符串"5"進(jìn)行比較亿乳,結(jié)果是 true葛假。全等操作符知道要比較的是兩種不同的數(shù)據(jù)類型聊训,因而直接返回 false。對于 1 和 true 也是如此:相等操作符認(rèn)為它們相等,而全等操作符認(rèn)為它們不相等妈候。
??使用全等和非全等操作符挂滓,可以避免發(fā)生因為使用相等和不相等操作符引發(fā)的類型轉(zhuǎn)換錯誤赶站,因此我們強(qiáng)烈推薦使用贝椿。
??容易發(fā)生類型轉(zhuǎn)換錯誤的另一個地方团秽,就是流控制語句习勤。像 if 之類的語句在確定下一步操作之前图毕,會自動把任何值轉(zhuǎn)換成布爾值予颤。尤其是 if 語句蛤虐,如果使用不當(dāng)驳庭,最容易出錯饲常。來看下面的例子贝淤。
function concat(str1, str2, str3){
var result = str1 + str2;
if (str3){ // 絕對不要這樣!!!
result += str3;
}
return result;
}
??這個函數(shù)的用意是拼接兩或三個字符串朽基,然后返回結(jié)果稼虎。其中渡蜻,第三個字符串是可選的茸苇,因此必須要檢查学密。
??未使用過的命名變量會自動被賦予 undefined 值腻暮。而 undefined 值可以被轉(zhuǎn)換成布爾值 false哭靖,因此這個函數(shù)中的 if 語句實際上只適用于提供了第三個參數(shù)的情況试幽。問題在于铺坞,并不是只有 undefined 才會被轉(zhuǎn)換成 false,也不是只有字符串值才可以轉(zhuǎn)換為 true擒滑。
??例如橘忱,假設(shè)第三個參數(shù)是數(shù)值 0钝诚,那么 if 語句的測試就會失敗凝颇,而對數(shù)值 1 的測試則會通過拧略。
??在流控制語句中使用非布爾值垫蛆,是極為常見的一個錯誤來源川无。為避免此類錯誤懦趋,就要做到在條件比較時切實傳入布爾值仅叫。實際上诫咱,執(zhí)行某種形式的比較就可以達(dá)到這個目的。例如,我們可以將前面的函數(shù)重寫如下边臼。
function concat(str1, str2, str3){
var result = str1 + str2;
if (typeof str3 == "string"){ // 恰當(dāng)?shù)谋容^
result += str3;
}
return result;
}
??在這個重寫后的函數(shù)中柠并,if 語句的條件會基于比較返回一個布爾值臼予。這個函數(shù)相對可靠得多窄锅,不容易受非正常值的影響入偷。
2. 數(shù)據(jù)類型錯誤
??JavaScript 是松散類型的疏之,也就是說锋爪,在使用變量和函數(shù)參數(shù)之前,不會對它們進(jìn)行比較以確保它們的數(shù)據(jù)類型正確年栓。為了保證不會發(fā)生數(shù)據(jù)類型錯誤某抓,只能依靠開發(fā)人員編寫適當(dāng)?shù)臄?shù)據(jù)類型檢測代碼否副。
??在將預(yù)料之外的值傳遞給函數(shù)的情況下,最容易發(fā)生數(shù)據(jù)類型錯誤曲尸。
??在前面的例子中另患,通過檢測第三個參數(shù)可以確保它是一個字符串鸦列,但是并沒有檢測另外兩個參數(shù)薯嗤。如果該函數(shù)必須要返回一個字符串骆姐,那么只要給它傳入兩個數(shù)值,忽略第三個參數(shù)归园,就可以輕易地導(dǎo)致它的執(zhí)行結(jié)果錯誤庸诱。類似的情況也存在于下面這個函數(shù)中桥爽。
// 不安全的函數(shù),任何非字符串值都會導(dǎo)致錯誤
function getQueryString(url){
var pos = url.indexOf("?");
if (pos > -1){
return url.substring(pos +1);
}
return "";
}
??這個函數(shù)的用意是返回給定 URL 中的查詢字符串缀去。為此缕碎,它首先使用 indexOf() 尋找字符串中的問號。如果找到了赊抖,利用 substring() 方法返回問號后面的所有字符串熏迹。
??上述例子中的兩個函數(shù)只能操作字符串,因此只要傳入其他數(shù)據(jù)類型的值就會導(dǎo)致錯誤捆昏。而添加一條簡單的類型檢測語句骗卜,就可以
確保函數(shù)不那么容易出錯。
function getQueryString(url){
if (typeof url == "string"){ // 通過檢查類型確保安全
var pos = url.indexOf("?");
if (pos > -1){
return url.substring(pos +1);
}
}
return "";
}
??重寫后的這個函數(shù)首先檢查了傳入的值是不是字符串遍烦。這樣,就確保了函數(shù)不會因為接收到非字符串值而導(dǎo)致錯誤罢猪。
??前一節(jié)提到過膳帕,在流控制語句中使用非布爾值作為條件很容易導(dǎo)致類型轉(zhuǎn)換錯誤。同樣恬砂,這樣做也經(jīng)常會導(dǎo)致數(shù)據(jù)類型錯誤泻骤。來看下面的例子。
// 不安全的函數(shù)趋惨,任何非數(shù)組值都會導(dǎo)致錯誤
function reverseSort(values){
if (values){ // 絕對不要這樣!!!
values.sort();
values.reverse();
}
}
??這個 reverseSort() 函數(shù)可以將數(shù)組反向排序讯嫂,其中用到了 sort() 和 reverse() 方法欧芽。
??對于 if 語句中的控制條件而言,任何會轉(zhuǎn)換為 true 的非數(shù)組值都會導(dǎo)致錯誤曲楚。另一個常見的錯誤就是將參數(shù)與 null 值進(jìn)行比較,如下所示载迄。
// 不安全的函數(shù)护昧,任何非數(shù)組值都會導(dǎo)致錯誤
function reverseSort(values){
if (values != null){ / /絕對不要這樣!!!
values.sort();
values.reverse();
}
}
??與 null 進(jìn)行比較只能確保相應(yīng)的值不是 null 和 undefined(這就相當(dāng)于使用相等和不相等操作)。要確保傳入的值有效绽榛,僅檢測 null 值是不夠的;因此届腐,不應(yīng)該使用這種技術(shù)。同樣围详,我們也不推薦將某個值與 undefined 作比較助赞。
??另一種錯誤的做法哩都,就是只針對要使用的某一個特性執(zhí)行特性檢測咐汞。來看下面的例子化撕。
// 還是不安全蟹瘾,任何非數(shù)組值都會導(dǎo)致錯誤
function reverseSort(values){
if (typeof values.sort == "function"){ // 絕對不要這樣!!!
values.sort();
values.reverse();
}
}
??在這個例子中,代碼首先檢測了參數(shù)中是否存在 sort() 方法众雷。這樣,如果傳入一個包含 sort() 方法的對象(而不是數(shù)組)當(dāng)然也會通過檢測编兄,但在調(diào)用 reverse() 函數(shù)時可能就會出錯了。
??在確切知道應(yīng)該傳入什么類型的情況下碰煌,最好是使用 instanceof 來檢測其數(shù)據(jù)類型,如下所示个少。
// 安全壳澳,非數(shù)組值將被忽略
function reverseSort(values){
if (values instanceof Array){ // 問題解決了
values.sort();
values.reverse();
}
}
??最后一個 reverseSort() 函數(shù)是安全的:它檢測了 values,以確保這個參數(shù)是 Array 類型的實例抹镊。這樣一來垮耳,就可以保證函數(shù)忽略任何非數(shù)組值。
??大體上來說,基本類型的值應(yīng)該使用 typeof 來檢測豌研,而對象的值則應(yīng)該使用 instanceof 來檢測。
??根據(jù)使用函數(shù)的方式霜浴,有時候并不需要逐個檢測所有參數(shù)的數(shù)據(jù)類型阴孟。但是,面向公眾的 API 則必須無條件地執(zhí)行類型檢查税迷,以確保函數(shù)始終能夠正常地執(zhí)行永丝。
3. 通信錯誤
??隨著 Ajax 編程的興起,Web 應(yīng)用程序在其生命周期內(nèi)動態(tài)加載信息或功能箭养,已經(jīng)成為一件司空見慣的事慕嚷。不過,JavaScript 與服務(wù)器之間的任何一次通信喝检,都有可能會產(chǎn)生錯誤嗅辣。
??第一種通信錯誤與格式不正確的 URL 或發(fā)送的數(shù)據(jù)有關(guān)。最常見的問題是在將數(shù)據(jù)發(fā)送給服務(wù)器之前挠说,沒有使用 encodeURIComponent() 對數(shù)據(jù)進(jìn)行編碼澡谭。例如,下面這個 URL 的格式就是不正確的:
http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d
??針對"redir="后面的所有字符串調(diào)用 encodeURIComponent() 就可以解決這個問題损俭,結(jié)果將產(chǎn)生如下字符串:
http://www.yourdomain.com/?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd
??對于查詢字符串蛙奖,應(yīng)該記住必須要使用 encodeURIComponent() 方法。為了確保這一點(diǎn)杆兵,有時候可以定義一個處理查詢字符串的函數(shù)外永,例如:
function addQueryStringArg(url, name, value){
if (url.indexOf("?") == -1){
url += "?";
} else {
url += "&";
}
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
??這個函數(shù)接收三個參數(shù):要追加查詢字符串的 URL、參數(shù)名和參數(shù)值拧咳。如果傳入的 URL 不包含問號,還要給它添加問號囚灼;否則骆膝,就要添加一個和號,因為有問號就意味著有其他查詢字符串灶体。然后阅签,再將經(jīng)過編碼的查詢字符串的名和值添加到 URL 后面⌒椋可以像下面這樣使用這個函數(shù):
var url = "http://www.somedomain.com";
var newUrl = addQueryStringArg(url, "redir", "http://www.someotherdomain.com?a=b&c=d");
alert(newUrl);
??使用這個函數(shù)而不是手工構(gòu)建 URL政钟,可以確保編碼正確并避免相關(guān)錯誤。
??另外樟结,在服務(wù)器響應(yīng)的數(shù)據(jù)不正確時养交,也會發(fā)生通信錯誤。
??運(yùn)用動態(tài)加載腳本和動態(tài)加載樣式這兩種技術(shù)都有可能遇到資源不可用的情況瓢宦。在沒有返回相應(yīng)資源的情況下碎连,F(xiàn)irefox、Chrome 和 Safari 會默默地失敗驮履,IE 和 Opera 則都會報錯鱼辙。
??然而,對于使用這兩種技術(shù)產(chǎn)生的錯誤玫镐,很難判斷和處理倒戏。在某些情況下,使用 Ajax 通信可以提供有關(guān)錯誤狀態(tài)的更多信息恐似。
2.6杜跷、區(qū)分致命錯誤和非致命錯誤
??任何錯誤處理策略中最重要的一個部分,就是確定錯誤是否致命。對于非致命錯誤葱椭,可以根據(jù)下列一或多個條件來確定:
- 不影響用戶的主要任務(wù)捂寿;
- 只影響頁面的一部分;
- 可以恢復(fù)孵运;
- 重復(fù)相同操作可以消除錯誤秦陋。
??本質(zhì)上,非致命錯誤并不是需要關(guān)注的問題治笨。
??例如,Yahoo! Mail(http://mail.yahoo.com)有一項功能驳概,允許用戶在其界面上發(fā)送手機(jī)短信。如果由于某種原因旷赖,發(fā)不了手機(jī)短信了顺又,那也不算是致命錯誤,因為并不是應(yīng)用程序的主要功能有問題等孵。用戶使用 Yahoo! Mail 主要是為了查收和撰寫電子郵件稚照。只在這個主要功能正常,就沒有理由打斷用戶俯萌。
??沒有必要因為發(fā)生了非致命錯誤而對用戶給出提示——可以把頁面中受到影響的區(qū)域替換掉果录,比如替換成說明相應(yīng)功能無法使用的消息。但是咐熙,如果因此打斷用戶弱恒,那確實沒有必要。
??致命錯誤棋恼,可以通過以下一或多個條件來確定:
- 應(yīng)用程序根本無法繼續(xù)運(yùn)行返弹;
- 錯誤明顯影響到了用戶的主要操作;
- 會導(dǎo)致其他連帶錯誤爪飘。
??要想采取適當(dāng)?shù)拇胧┮迤穑仨氁?JavaScript 在什么情況下會發(fā)生致命錯誤。在發(fā)生致命錯誤時悦施,應(yīng)該立即給用戶發(fā)送一條消息并扇,告訴他們無法再繼續(xù)手頭的事情了。假如必須刷新頁面才能讓應(yīng)用程序正常運(yùn)行抡诞,就必須通知用戶穷蛹,同時給用戶提供一個點(diǎn)擊即可刷新頁面的按鈕。
??區(qū)分非致命錯誤和致命錯誤的主要依據(jù)昼汗,就是看它們對用戶的影響肴熏。設(shè)計良好的代碼,可以做到應(yīng)用程序某一部分發(fā)生錯誤不會不必要地影響另一個實際上毫不相干的部分顷窒。
??例如蛙吏,My Yahoo!(http://my.yahoo.com)的個性化主頁上包含了很多互不依賴的模塊源哩。如果每個模塊都需要通過 JavaScript 調(diào)用來初始化,那么你可能會看到類似下面這樣的代碼:
for (var i=0, len=mods.length; i < len; i++){
mods[i].init(); // 可能會導(dǎo)致致命錯誤
}
??表面上看鸦做,這些代碼沒什么問題:依次對每個模塊調(diào)用 init() 方法励烦。問題在于,任何模塊的 init() 方法如果出錯泼诱,都會導(dǎo)致數(shù)組中后續(xù)的所有模塊無法再進(jìn)行初始化坛掠。
??從邏輯上說,這樣編寫代碼沒有什么意義治筒。畢竟屉栓,每個模塊相互之間沒有依賴關(guān)系,各自實現(xiàn)不同功能耸袜∮讯啵可能會導(dǎo)致致命錯誤的原因是代碼的結(jié)構(gòu)。不過堤框,經(jīng)過下面這樣修改域滥,就可以把所有模塊的錯誤變成非致命的:
for (var i=0, len=mods.length; i < len; i++){
try {
mods[i].init();
} catch (ex) {
// 在這里處理錯誤
}
}
??通過在 for 循環(huán)中添加 try-catch 語句,任何模塊初始化時出錯蜈抓,都不會影響其他模塊的初始化骗绕。
??在以上重寫的代碼中,如果有錯誤發(fā)生资昧,相應(yīng)的錯誤將會得到獨(dú)立的處理,并不會影響到用戶的體驗荆忍。
2.7逊躁、把錯誤記錄到服務(wù)器
??開發(fā) Web 應(yīng)用程序過程中的一種常見的做法姆坚,就是集中保存錯誤日志,以便查找重要錯誤的原因。
??例如數(shù)據(jù)庫和服務(wù)器錯誤都會定期寫入日志僻孝,而且會按照常用 API 進(jìn)行分類。
??在復(fù)雜的 Web 應(yīng)用程序中添怔,我們同樣推薦你把 JavaScript 錯誤也回寫到服務(wù)器蝎宇。換句話說,也要將這些錯誤寫入到保存服務(wù)器端錯誤的地方蟋软,只不過要標(biāo)明它們來自前端镶摘。把前后端的錯誤集中起來,能夠極大地方便對數(shù)據(jù)的分析岳守。
??要建立這樣一種 JavaScript 錯誤記錄系統(tǒng)凄敢,首先需要在服務(wù)器上創(chuàng)建一個頁面(或者一個服務(wù)器入口點(diǎn)),用于處理錯誤數(shù)據(jù)湿痢。這個頁面的作用無非就是從查詢字符串中取得數(shù)據(jù)涝缝,然后再將數(shù)據(jù)寫入錯誤日志中。這個頁面可能會使用如下所示的函數(shù):
function logError(sev, msg){
var img = new Image();
img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + encodeURIComponent(msg);
}
??這個 logError() 函數(shù)接收兩個參數(shù):表示嚴(yán)重程度的數(shù)值或字符串(視所用系統(tǒng)而異)及錯誤消息。其中拒逮,使用了 Image 對象來發(fā)送請求罐氨,這樣做非常靈活,主要表現(xiàn)如下幾方面滩援。
- 所有瀏覽器都支持 Image 對象栅隐,包括那些不支持 XMLHttpRequest 對象的瀏覽器。
- 可以避免跨域限制狠怨。通常都是一臺服務(wù)器要負(fù)責(zé)處理多臺服務(wù)器的錯誤约啊,而這種情況下使用 XMLHttpRequest 是不行的。
- 在記錄錯誤的過程中出問題的概率比較低佣赖。大多數(shù) Ajax 通信都是由 JavaScript 庫提供的包裝函數(shù)來處理的恰矩,如果庫代碼本身有問題,而你還在依賴該庫記錄錯誤憎蛤,可想而知外傅,錯誤消息是不可能得到記錄的。
??只要是使用 try-catch 語句俩檬,就應(yīng)該把相應(yīng)錯誤記錄到日志中萎胰。來看下面的例子。
for (var i=0, len=mods.length; i < len; i++){
try {
mods[i].init();
} catch (ex){
logError("nonfatal", "Module init failed: " + ex.message);
}
}
??在這里棚辽,一旦模塊初始化失敗技竟,就會調(diào)用 logError()。第一個參數(shù)是"nonfatal"(非致命)屈藐,表示錯誤的嚴(yán)重程度榔组。第二個參數(shù)是上下文信息加上真正的 JavaScript 錯誤消息。記錄到服務(wù)器中的錯誤消息應(yīng)該盡可能多地帶有上下文信息联逻,以便鑒別導(dǎo)致錯誤的真正原因搓扯。
3、調(diào)試技術(shù)
??在不那么容易找到 JavaScript 調(diào)試程序的年代包归,開發(fā)人員不得不發(fā)揮自己的創(chuàng)造力锨推,通過各種方法來調(diào)試自己的代碼。結(jié)果公壤,就出現(xiàn)了以這樣或那樣的方式置入代碼换可,從而輸出調(diào)試信息的做法。
??其中厦幅,最常見的做法就是在要調(diào)試的代碼中隨處插入 alert() 函數(shù)锦担。但這種做法一方面比較麻煩(調(diào)試之后還需要清理),另一方面還可能引入新問題(想象一下把某個 alert() 函數(shù)遺留在產(chǎn)品代碼中的結(jié)果)慨削。如今洞渔,已經(jīng)有了很多更好的調(diào)試工具套媚,因此我們也不再建議在調(diào)試中使用 alert() 了。
3.1磁椒、將消息記錄到控制臺
??IE8堤瘤、Firefox、Opera浆熔、Chrome 和 Safari 都有 JavaScript 控制臺本辐,可以用來查看 JavaScript 錯誤。而且医增,在這些瀏覽器中慎皱,都可以通過代碼向控制臺輸出消息。
??對 Firefox 而言叶骨,需要安裝 Firebug(www.getfirebug.com)茫多,因為 Firefox 要使用 Firebug 的控制臺。
??對 IE8忽刽、Firefox天揖、Chrome 和 Safari 來說,則可以通過 console 對象向 JavaScript 控制臺中寫入消息跪帝,這個對象具有下列方法今膊。
- error(message):將錯誤消息記錄到控制臺
- info(message):將信息性消息記錄到控制臺
- log(message):將一般消息記錄到控制臺
- warn(message):將警告消息記錄到控制臺
??在 IE8、Firebug伞剑、Chrome 和 Safari 中斑唬,用來記錄消息的方法不同,控制臺中顯示的錯誤消息也不一樣黎泣。錯誤消息帶有紅色圖標(biāo)赖钞,而警告消息帶有黃色圖標(biāo)。以下函數(shù)展示了使用控制臺輸出消息的一個示例聘裁。
function sum(num1, num2){
console.log("Entering sum(), arguments are " + num1 + "," + num2);
console.log("Before calculation");
var result = num1 + num2;
console.log("After calculation");
console.log("Exiting sum()");
return result;
}
??在調(diào)用這個 sum() 函數(shù)時,控制臺中會出現(xiàn)一些消息弓千,可以用來輔助調(diào)試衡便。
??Opera 10.5 之前的版本中,JavaScript 控制臺可以通過 opera.postError() 方法來訪問洋访。這個方法接受一個參數(shù)镣陕,即要寫入到控制臺中的參數(shù),其用法如下姻政。
function sum(num1, num2){
opera.postError("Entering sum(), arguments are " + num1 + "," + num2);
opera.postError("Before calculation");
var result = num1 + num2;
opera.postError("After calculation");
opera.postError("Exiting sum()");
return result;
}
??別看 opera.postError() 方法的名字好像是只能輸出錯誤呆抑,但實際上能通過它向 JavaScript 控制臺中寫入任何信息。
??還有一種方案是使用 LiveConnect汁展,也就是在 JavaScript 中運(yùn)行 Java 代碼鹊碍。Firefox厌殉、Safari 和 Opera 都支持 LiveConnect,因此可以操作 Java 控制臺侈咕。例如公罕,通過下列代碼就可以在 JavaScript 中把消息寫入到 Java 控制臺。
java.lang.System.out.println("Your message");
??可以用這行代碼替代 console.log() 或 opera.postError()耀销,如下所示楼眷。
function sum(num1, num2){
java.lang.System.out.println("Entering sum(), arguments are " + num1 + "," + num2);
java.lang.System.out.println("Before calculation");
var result = num1 + num2;
java.lang.System.out.println("After calculation");
java.lang.System.out.println("Exiting sum()");
return result;
}
??如果系統(tǒng)設(shè)置恰當(dāng),可以在調(diào)用 LiveConnect 時就立即顯示 Java 控制臺熊尉。
??不存在一種跨瀏覽器向 JavaScript 控制臺寫入消息的機(jī)制罐柳,但下面的函數(shù)倒可以作為統(tǒng)一的接口。
function log(message){
if (typeof console == "object"){
console.log(message);
} else if (typeof opera == "object"){
opera.postError(message);
} else if (typeof java == "object" && typeof java.lang == "object"){
java.lang.System.out.println(message);
}
}
??這個 log() 函數(shù)檢測了哪個 JavaScript 控制臺接口可用狰住,然后使用相應(yīng)的接口张吉。可以在任何瀏覽器中安全地使用這個函數(shù)转晰,不會導(dǎo)致任何錯誤芦拿,例如:
function sum(num1, num2){
log("Entering sum(), arguments are " + num1 + "," + num2);
log("Before calculation");
var result = num1 + num2;
log("After calculation");
log("Exiting sum()");
return result;
}
??向 JavaScript 控制臺中寫入消息可以輔助調(diào)試代碼,但在發(fā)布應(yīng)用程序時查邢,還必須要移除所有消息蔗崎。
??在部署應(yīng)用程序時,可以通過手工或通過特定的代碼處理步驟來自動完成清理工作扰藕。
??記錄消息要比使用 alert() 函數(shù)更可取缓苛,因為警告框會阻斷程序的執(zhí)行,而在測定異步處理對時間的影響時邓深,使用警告框會影響結(jié)果未桥。
3.2、將消息記錄到當(dāng)前頁面
??另一種輸出調(diào)試消息的方式芥备,就是在頁面中開辟一小塊區(qū)域冬耿,用以顯示消息。這個區(qū)域通常是一個元素萌壳,而該元素可以總是出現(xiàn)在頁面中亦镶,但僅用于調(diào)試目的;也可以是一個根據(jù)需要動態(tài)創(chuàng)建的元素袱瓮。例如缤骨,可以將 log() 函數(shù)修改為如下所示:
function log(message){
var console = document.getElementById("debuginfo");
if (console === null){
console = document.createElement("div");
console.id = "debuginfo";
console.style.background = "#dedede";
console.style.border = "1px solid silver";
console.style.padding = "5px";
console.style.width = "400px";
console.style.position = "absolute";
console.style.right = "0px";
console.style.top = "0px";
document.body.appendChild(console);
}
console.innerHTML += "<p>" + message + "</p>";
}
??這個修改后的 log() 函數(shù)首先檢測是否已經(jīng)存在調(diào)試元素,如果沒有則會新創(chuàng)建一個<div>元素尺借,并為該元素應(yīng)用一些樣式绊起,以便與頁面中的其他元素區(qū)別開。
??然后燎斩,又使用 innerHTML 將消息寫入到這個<div>元素中虱歪。結(jié)果就是頁面中會有一小塊區(qū)域顯示錯誤消息蜂绎。這種技術(shù)在不支持 JavaScript 控制臺的 IE7 及更早版本或其他瀏覽器中十分有用。
??與把錯誤消息記錄到控制臺相似实蔽,把錯誤消息輸出到頁面的代碼也要在發(fā)布前刪除荡碾。
3.3、拋出錯誤
??如前所述局装,拋出錯誤也是一種調(diào)試代碼的好辦法坛吁。如果錯誤消息很具體,基本上就可以把它當(dāng)作確定錯誤來源的依據(jù)铐尚。但這種錯誤消息必須能夠明確給出導(dǎo)致錯誤的原因拨脉,才能省去其他調(diào)試操作。來看下面的函數(shù):
function divide(num1, num2){
return num1 / num2;
}
??這個簡單的函數(shù)計算兩個數(shù)的除法宣增,但如果有一個參數(shù)不是數(shù)值玫膀,它會返回 NaN。類似這樣簡單的計算如果返回 NaN爹脾,就會在 Web 應(yīng)用程序中導(dǎo)致問題帖旨。對此,可以在計算之前灵妨,先檢測每個參數(shù)是否都是數(shù)值解阅。例如:
function divide(num1, num2){
if (typeof num1 != "number" || typeof num2 != "number") {
throw new Error("divide(): Both arguments must be numbers.");
}
return num1 / num2;
}
??在此,如果有一個參數(shù)不是數(shù)值泌霍,就會拋出錯誤货抄。錯誤消息中包含了函數(shù)的名字,以及導(dǎo)致錯誤的真正原因朱转。瀏覽器只要報告了這個錯誤消息蟹地,我們就可以立即知道錯誤來源及問題的性質(zhì)。
??相對來說藤为,這種具體的錯誤消息要比那些泛泛的瀏覽器錯誤消息更有用怪与。
??對于大型應(yīng)用程序來說,自定義的錯誤通常都使用 assert() 函數(shù)拋出缅疟。這個函數(shù)接受兩個參數(shù)分别,一個是求值結(jié)果應(yīng)該為 true 的條件,另一個是條件為 false 時要拋出的錯誤窿吩。以下就是一個非常基本的 assert() 函數(shù)错览。
function assert(condition, message){
if (!condition){
throw new Error(message);
}
}
??可以用這個 assert() 函數(shù)代替某些函數(shù)中需要調(diào)試的 if 語句纫雁,以便輸出錯誤消息。下面是使用這個函數(shù)的例子倾哺。
function divide(num1, num2){
assert(typeof num1 == "number" && typeof num2 == "number", "divide(): Both arguments must be numbers.");
return num1 / num2;
}
??可見轧邪,使用 assert() 函數(shù)可以減少拋出錯誤所需的代碼量刽脖,而且也比前面的代碼更容易看懂。
4忌愚、常見的 IE 錯誤
??多年以來曲管,IE 一直都是最難于調(diào)試 JavaScript 錯誤的瀏覽器。IE 給出的錯誤消息一般很短又語焉不詳硕糊,而且上下文信息也很少院水,有時甚至一點(diǎn)都沒有。
??但作為用戶最多的瀏覽器简十,如何看懂 IE 給出的錯誤也是最受關(guān)注的檬某。下面幾小節(jié)將分別探討一些在 IE 中難于調(diào)試的 JavaScript 錯誤。
4.1螟蝙、操作終止
??在 IE8 之前的版本中恢恼,存在一個相對于其他瀏覽器而言,最令人迷惑胰默、討厭场斑,也最難于調(diào)試的錯誤:操作終止(operation aborted)。
??在修改尚未加載完成的頁面時牵署,就會發(fā)生操作終止錯誤漏隐。發(fā)生錯誤時,會出現(xiàn)一個模態(tài)對話框碟刺,告訴你“操作終止锁保。”單擊確定(OK)按鈕半沽,則卸載整個頁面爽柒,繼而顯示一張空白屏幕;此時要進(jìn)行調(diào)試非常困難者填。下面的示例將會導(dǎo)致操作終止錯誤浩村。
<!DOCTYPE html>
<html>
<head>
<title>Operation Aborted Example</title>
</head>
<body>
<p>The following code should cause an Operation Aborted error in IE versions
prior to 8.</p>
<div>
<script type="text/javascript">
document.body.appendChild(document.createElement("div"));
</script>
</div>
</body>
</html>
??上述例子中存在的問題是:JavaScript 代碼在頁面尚未加載完畢時就要修改 document.body,而且<script>元素還不是<body>元素的直接子元素占哟。
??準(zhǔn)確一點(diǎn)說心墅,當(dāng)<script>節(jié)點(diǎn)被包含在某個元素中,而且 JavaScript 代碼又要使用 appendChild()榨乎、innerHTML 或其他 DOM 方法修改該元素的父元素或祖先元素時怎燥,將會發(fā)生操作終止錯誤(因為只能修改已經(jīng)加載完畢的元素)。
??要避免這個問題蜜暑,可以等到目標(biāo)元素加載完畢后再對它進(jìn)行操作铐姚,或者使用其他操作方法。例如肛捍,為 document.body 添加一個絕對定位在頁面上的覆蓋層隐绵,就是一種非常常見的操作之众。
??通常,開發(fā)人員都是使用 appendChild() 方法來添加這個元素的依许,但換成使用 insertBefore() 方法也很容易棺禾。因此,只要修改前面例子中的一行代碼峭跳,就可以避免操作終止錯誤膘婶。
<!DOCTYPE html>
<html>
<head>
<title>Operation Aborted Example</title>
</head>
<body>
<p>The following code should not cause an Operation Aborted error in IE
versions prior to 8.</p>
<div>
<script type="text/javascript">
document.body.insertBefore(document.createElement("div"),
document.body.firstChild);
</script>
</div>
</body>
</html>
??在這個例子中,新的<div>元素被添加到 document.body 的開頭部分而不是末尾坦康。因為完成這一操作所需的所有信息在腳本運(yùn)行時都是已知的竣付,所以這不會引發(fā)錯誤。
??除了改變方法之外滞欠,還可以把<script>元素從包含元素中移出來古胆,直接作為<body>的子元素。例如:
<!DOCTYPE html>
<html>
<head>
<title>Operation Aborted Example</title>
</head>
<body>
<p>The following code should not cause an Operation Aborted error in IE
versions prior to 8.</p>
<div>
</div>
<script type="text/javascript">
document.body.appendChild(document.createElement("div"));
</script>
</body>
</html>
??這一次也不會發(fā)生錯誤筛璧,因為腳本修改的是它的直接父元素逸绎,而不再是間接的祖先元素。在同樣的情況下夭谤,IE8 不再拋出操作終止錯誤棺牧,而是拋出常規(guī)的 JavaScript 錯誤,帶有如下錯誤消息:
HTML Parsing Error: Unable to modify the parent container element
before the child element is closed (KB927917).
??不過朗儒,雖然瀏覽器拋出的錯誤不同颊乘,但解決方案仍然是一樣的。
4.2醉锄、無效字符
??根據(jù)語法乏悄,JavaScript 文件必須只包含特定的字符。在 JavaScript 文件中存在無效字符時恳不,IE 會拋出無效字符(invalid character)錯誤檩小。
??所謂無效字符,就是 JavaScript 語法中未定義的字符烟勋。例如规求,有一個很像減號但卻由 Unicode 值 8211 表示的字符(\u2013),就不能用作常規(guī)的減號(ASCII 編碼為 45)卵惦,因為 JavaScript 語法中沒有定義該字符阻肿。
??這個字符通常是在 Word 文檔中自動插入的。如果你的代碼是從 Word 文檔中復(fù)制到文本編輯器中沮尿,然后又在 IE 中運(yùn)行的丛塌,那么就可能會遇到無效字符錯誤。
??其他瀏覽器對無效字符做出的反應(yīng)與 IE 類似,F(xiàn)irefox 會拋出非法字符(illegal character)錯誤姨伤,Safari 會報告發(fā)生了語法錯誤,而 Opera 則會報告發(fā)生了 ReferenceError(引用錯誤)庸疾,因為它會將無效字符解釋為未定義的標(biāo)識符乍楚。
4.3、未找到成員
??如前所述届慈,IE 中的所有 DOM 對象都是以 COM 對象徒溪,而非原生 JavaScript 對象的形式實現(xiàn)的。這會導(dǎo)致一些與垃圾收集相關(guān)的非常奇怪的行為金顿。
??IE 中的未找到成員(Member not found)錯誤臊泌,就是由于垃圾收集例程配合錯誤所直接導(dǎo)致的。
??具體來說揍拆,如果在對象被銷毀之后渠概,又給該對象賦值,就會導(dǎo)致未找到成員錯誤嫂拴。而導(dǎo)致這個錯誤的播揪,一定是 COM 對象。
??發(fā)生這個錯誤的最常見情形是使用 event 對象的時候筒狠。IE 中的 event 對象是 window 的屬性猪狈,該對象在事件發(fā)生時創(chuàng)建,在最后一個事件處理程序執(zhí)行完畢后銷毀辩恼。假設(shè)你在一個閉包中使用了 event 對象雇庙,而該閉包不會立即執(zhí)行,那么在將來調(diào)用它并給 event 的屬性賦值時灶伊,就會導(dǎo)致未找到成員錯誤疆前,如下面的例子所示。
document.onclick = function(){
var event = window.event;
setTimeout(function(){
event.returnValue = false; // 未找到成員錯誤
}, 1000);
};
??在這段代碼中谁帕,我們將一個單擊事件處理程序指定給了文檔峡继。在事件處理程序中,window.event 被保存在 event 變量中匈挖。
??然后碾牌,傳入 setTimeout() 中的閉包里又包含了 event 變量。當(dāng)單擊事件處理程序執(zhí)行完畢后儡循,event 對象就會被銷毀舶吗,因而閉包中引用對象的成員就成了不存在的了。換句話說择膝,由于不能在 COM 對象被銷毀之后再給其成員賦值誓琼,在閉包中給 returnValue 賦值就會導(dǎo)致未找到成員錯誤。
4.4、未知運(yùn)行時錯誤
??當(dāng)使用 innerHTML 或 outerHTML 以下列方式指定 HTML 時腹侣,就會發(fā)生未知運(yùn)行時錯誤(Unknown runtime error):一是把塊元素插入到行內(nèi)元素時叔收,二是訪問表格任意部分(<table>、<tbody>等)的任意屬性時傲隶。例如饺律,從技術(shù)角度說,<span>標(biāo)簽不能包含<div>之類的塊級元素跺株,因此下面的代碼就會導(dǎo)致未知運(yùn)行時錯誤:
span.innerHTML = "<div>Hi</div>"; // 這里复濒,span 包含了<div>元素
??在遇到把塊級元素插入到不恰當(dāng)位置的情況時,其他瀏覽器會嘗試糾正并隱藏錯誤乒省,而 IE 在這一點(diǎn)上反倒很較真兒巧颈。
4.5、語法錯誤
??通常袖扛,只要 IE 一報告發(fā)生了語法錯誤(syntax error)砸泛,都可以很快找到錯誤的原因。這時候蛆封,原因可能是代碼中少了一個分號晾嘶,或者花括號前后不對應(yīng)。然而娶吞,還有一種原因不十分明顯的情況需要格外注意垒迂。
??如果你引用了外部的 JavaScript 文件,而該文件最終并沒有返回 JavaScript 代碼妒蛇,IE 也會拋出語法錯誤机断。
??例如,<script>元素的 src 特性指向了一個 HTML 文件绣夺,就會導(dǎo)致語法錯誤吏奸。報告語法錯誤的位置時,通常都會說該錯誤位于腳本第一行的第一個字符處陶耍。
??Opera 和 Safari 也會報告語法錯誤奋蔚,但它們會給出導(dǎo)致問題的外部文件的信息;IE 就不會給出這個信息烈钞,因此就需要我們自己重復(fù)檢查一遍引用的外部 JavaScript 文件泊碑。
??但 Firefox 會忽略那些被當(dāng)作 JavaScript 內(nèi)容嵌入到文檔中的非 JavaScript 文件中的解析錯誤。
??在服務(wù)器端組件動態(tài)生成 JavaScript 的情況下毯欣,比較容易出現(xiàn)這種錯誤馒过。很多服務(wù)器端語言都會在發(fā)生運(yùn)行時錯誤時,向輸出中插入 HTML 代碼酗钞,而這種包含 HTML 的輸出很容易就會違反 JavaScript 語法腹忽。如果在追查語法錯誤時遇到了麻煩来累,我們建議你再仔細(xì)檢查一遍引用的外部文件,確保這些文件中沒有包含服務(wù)器因錯誤而插入到其中的 HTML窘奏。
4.6嘹锁、系統(tǒng)無法找到指定資源
??系統(tǒng)無法找到指定資源(The system cannot locate the resource specified)這種說法,恐怕要算是 IE 給出的最有價值的錯誤消息了着裹。
??在使用 JavaScript 請求某個資源 URL兼耀,而該 URL 的長度超過了 IE 對 URL
最長不能超過 2083 個字符的限制時,就會發(fā)生這個錯誤求冷。IE 不僅限制 JavaScript 中使用的 URL 的長度,而且也限制用戶在瀏覽器自身中使用的 URL 長度(其他瀏覽器對 URL 的限制沒有這么嚴(yán)格)窍霞。
??IE 對 URL 路徑還有一個不能超過 2048 個字符的限制匠题。下面的代碼將會導(dǎo)致錯誤。
function createLongUrl(url){
var s = "?";
for (var i=0, len=2500; i < len; i++){
s += "a";
}
return url + s;
}
var x = new XMLHttpRequest();
x.open("get", createLongUrl("http://www.somedomain.com/"), true);
x.send(null);
??在上述例子中但金,XMLHttpRequest 對象試圖向一個超出最大長度限制的 URL 發(fā)送請求韭山。在調(diào)用 open() 方法時,就會發(fā)生錯誤冷溃。避免這個問題的辦法钱磅,無非就是通過給查詢字符串參數(shù)起更短的名字,或者減少不必要的數(shù)據(jù)似枕,來縮短查詢字符串的長度盖淡。
??另外,還可以把請求方法改為 POST凿歼,通過請求體而不是查詢字符串來發(fā)送數(shù)據(jù)褪迟。
小結(jié)
??錯誤處理對于今天復(fù)雜的 Web 應(yīng)用程序開發(fā)而言至關(guān)重要。不能提前預(yù)測到可能發(fā)生的錯誤答憔,不能提前采取恢復(fù)策略味赃,可能導(dǎo)致較差的用戶體驗,最終引發(fā)用戶不滿虐拓。
??多數(shù)瀏覽器在默認(rèn)情況下都不會向用戶報告錯誤心俗,因此在開發(fā)和調(diào)試期間需要啟用瀏覽器的錯誤報告功能。然而蓉驹,在投入運(yùn)行的產(chǎn)品代碼中城榛,則不應(yīng)該再有諸如此類的錯誤報告出現(xiàn)。
??下面是幾種避免瀏覽器響應(yīng) JavaScript 錯誤的方法态兴。
- 在可能發(fā)生錯誤的地方使用 try-catch 語句吠谢,這樣你還有機(jī)會以適當(dāng)?shù)姆绞綄﹀e誤給出響應(yīng),而不必沿用瀏覽器處理錯誤的機(jī)制诗茎。
- 使用 window.onerror 事件處理程序工坊,這種方式可以接受 try-catch 不能處理的所有錯誤(僅限于 IE献汗、Firefox 和 Chrome)。
??另外王污,對任何 Web 應(yīng)用程序都應(yīng)該分析可能的錯誤來源罢吃,并制定處理錯誤的方案。
- 首先昭齐,必須要明確什么是致命錯誤尿招,什么是非致命錯誤。
- 其次阱驾,再分析代碼就谜,以判斷最可能發(fā)生的錯誤。JavaScript 中發(fā)生錯誤的主要原因如下里覆。
- 類型轉(zhuǎn)換
- 未充分檢測數(shù)據(jù)類型
- 發(fā)送給服務(wù)器或從服務(wù)器接收到的數(shù)據(jù)有錯誤
??IE丧荐、Firefox、Chrome喧枷、Opera 和 Safari 都有 JavaScript 調(diào)試器虹统,有的是內(nèi)置的,有的是以需要下載的擴(kuò)展形式存在的隧甚。這些調(diào)試器都支持設(shè)置斷點(diǎn)车荔、控制代碼執(zhí)行及在運(yùn)行時檢測變量的值。