錯誤處理在處理程序設計中的重要性是毋庸置疑的,任何有影響力的web應用程序都需要一套完善的錯誤處理機制。當然,大多數佼佼者確實做到了這一點,但通常只有服務器端應用程序才能做到如此。實際上,服務器端團隊往往會在錯誤處理機制上投入較大精力,通常要考慮按照類型、頻率,或者其他重要的標准對錯誤進行分類。這樣一來,開發人員就能夠理解用戶在使用簡單數據庫查詢或者報告生成腳本時,應用程序可能會出現的問題。
雖然客戶端應用程序的錯誤處理也同樣重要,但真正受到重視,還是最近幾年的事。實際上,我們要面對這樣一個不爭的事實:使用web的絕大多數人都不是技術高手,其中甚至很多人根本就不明白瀏覽器到底是什么,更不用說讓他們說喜歡哪一個了。每個瀏覽器在發生JavaScript錯誤時的行為都或多或少有一些差異。有的會顯示小圖標,有的則什么動靜都沒有,瀏覽器對JavaScript錯誤的這些默認行為對最終用戶而言,毫無規律可循。最理想的情況下,用戶遇到錯誤搞不清為什么,他們會再試着重做一次,最糟糕的情況下,用戶會惱羞成怒,一去不復返了。良好的錯誤處理機制可以讓用戶及時得到提醒,知道到底發生了什么事,因而不會驚慌失措。為此,作為開發人員,我們必須理解在處理JavaScript錯誤的時候,都有哪些手段和工具可以利用。
一,try-catch 語句
ECMA-262第3版引入了try-catch語句,作為JavaScript中處理異常的一種標准方式。基本的語法如下所示,顯而易見,這與Java中的try-catch語句是完全相同的:
try { //可能會導致錯誤的代碼 } catch (error) { //在錯誤發生時怎么處理 }
也就是說,我們應該把所有可能會拋出錯誤的代碼都放在try語句快中,而把那些用於錯誤處理的代碼放在catch塊中,例如:
try { window.someNonexistentFunction(); //調用不存在的函數 } catch (error) { alert('An error happened!'); }
如果try塊中的任何代碼發生了錯誤,就會立即退出代碼執行過程,然后接着執行catch塊,此時,catch塊會接收到一個包含錯誤信息的對象。與在其他語言中不同的是,即使你不想使用這個錯誤對象,也要給它起個名字。這個對象中包含的實際信息會因瀏覽器而異,但共同的是有一個保存着錯誤信息的message屬性。ECMA-262還規定了一個保存着錯誤類型的name屬性,當前所有瀏覽器都支持這個屬性(Opera9之前的版本不支持這個屬性)。因此,在發生錯誤時,就可以像下面這樣實事求是地顯示瀏覽器給出的信息:
try { window.someNonexistentFunction(); //調用不存在的函數 } catch (error) { alert(error.message); }
這個例子在向用戶顯示錯誤信息時,使用了錯誤對象的message屬性,這個message屬性是唯一一個能保證所有瀏覽器都支持的屬性,除此之外,IE、Firefox、Safari、Chrome以及Opera都為事件對象添加了其他相關信息。IE添加了與message屬性完全相同的description屬性,還添加了保存着內部錯誤數量的number屬性。Firefox添加了fileName、lineNumber和stack(包含棧跟蹤信息)屬性。Safari添加了line(表示行號)、sourceId(表示內部錯誤代碼)和sourceUrl屬性。當然,在跨瀏覽器編程時,最好還是只使用message屬性。
1,finally子句
雖然在try-catch語句中是可選的,但finally子句一經使用,其代碼無論如何都會執行。換句話說,try語句塊中的代碼全部正常執行,finally子句會執行;如果因為出錯而執行了catch語句塊,finally子句照樣還會執行。只要代碼中包含finally子句,則無論try或catch語句塊中包含什么樣的代碼——甚至return語句,都不會阻止finally子句的執行。來看下面這個函數:
function testFinally() { try { return 2; } catch (error) { return 1; } finally { return 0; } } testFinally(); //0
這個函數在try-catch語句的每一部分都放了一條return語句。表面上看,調用這個函數會返回2,因為返回2個return語句位於try語句塊中,而執行該語句又不會出錯。可是,由於最后還有一個finally子句,結果就會導致該return語句被忽略,也就是說,調用這個函數只能返回0,如果把finally子句拿掉,這個函數將返回2.(請讀者務必要記住,只要代碼中包含finally子句,那么無論try還是catch語句塊中的return語句都將被忽略。因此,在使用finally子句之前,一定要非常清楚你想要代碼怎么樣)
2,錯誤類型
執行代碼期間可能會發生的錯誤有多種類型,每種錯誤都有對應的錯誤類型,而當錯誤發生時,就會拋出相應類型的錯誤對象。ECMA-262定義了下列7種錯誤類型:
01, Error
02, EvalError
03, RangeError
04, ReferenceError
05, SyntaxError
06, TypeError
07, URIError
其中,Error是基類型,其他錯誤類型都繼承自該類型,因此,所有錯誤類型共享了一組相同的屬性(錯誤對象中的方法全是默認的對象方法)。Error類型的錯誤很少見,如果有也是瀏覽器拋出的;這個基類型的主要目的是供開發人員拋出自定義錯誤。
EvalError類型的錯誤是在使用eval()函數而發生異常時拋出。ECMA-262中對這個錯誤有如下描述:“表示全局函數eval的使用方式與其定義不相符。“除此之外,並沒有救到底什么情況會引發這種錯誤給出說明。在實際開發中碰到這種錯誤的可能性並不大。
RangeError類型的錯誤會在數值超出相應范圍時觸發。例如,在定義數組時,如果指定了數組不支持的項數(如-20或Number.MAX_VALUE),就會觸發這種錯誤。下面是具體的例子:
var items1 = new Array(-20); //VM77:1 Uncaught RangeError: Invalid array length(…) var items2 = new Array(Number.MAX_VALUE); //VM79:1 Uncaught RangeError: Invalid array length(…)
JavaScript中經常會出現這種范圍錯誤。
在找不到對象的情況下,會發生ReferenceError(這種情況下,會直接導致人所共知的“object expected"瀏覽器錯誤)。通常,在訪問不存在的變量時,就會發生這種錯誤,例如:
var obj = x; //VM112:1 Uncaught ReferenceError: x is not defined(…) 在x未聲明的情況下拋出ReferenceError
至於SyntaxError,當我們把語法錯誤的JavaScript字符串傳入eval()函數時,就會導致此類錯誤,例如:
eval('a ++ b'); //VM114:1 Uncaught SyntaxError: Unexpected identifier(…)
如果語法錯誤的代碼出現在eval()函數之外,則不太可能發生SyntaxError,因為此時的語法錯誤導致JavaScript代碼立即停止執行。
TypeError類型在JavaScript中經常用到,在變量中保存着意外的類型時,或者在訪問不存在的方法時,都會導致這種錯誤。錯誤的原因雖然多種多樣,但歸根結底還是由於在執行特定於類型的操作時,變量的類型並不符合要求所致。下面來看幾個例子:

最常發生類型錯誤的情況,就是傳遞給函數的參數事先未經檢查,結果傳入類型與預期類型不相符。
在使用encodeURI()或decodeURI(),而URI格式不正確時,就會導致URIError錯誤,這種錯誤也很少見,因為前面說的這兩個函數的容錯性非常高。
利用不同的錯誤類型,可以熟悉更多有關異常的信息,從而有助於對錯誤作出恰當的處理,要想知道錯誤的類型,可以像下面這樣在try-catch語句的catch語句中使用instanceof操作符:
try { someFunction(); } catch (error) { if (error instanceof TypeError) { //處理類型錯誤 } else if (error instanceof ReferenceError) { //處理引用錯誤 } else { //處理其他類型的錯誤 } }
在跨瀏覽器編程中,檢查錯誤類型是確定處理方式的最簡便途徑,包含在message屬性中的錯誤消息會因瀏覽器而異。
3,善用try-catch
當try-catch語句中發生錯誤時,瀏覽器會認為錯誤已經被處理了。因而不會通過前面討論的機制記錄或報告錯誤。對於那些不要求用戶懂技術,也不需要用戶理解錯誤的Web應用程序,這應該說是個理想的結果。不過,try-catch能夠讓我們實現自己的錯誤處理機制。
使用try-catch最適合處理那些我們無法控制的錯誤,假設你在使用一個大型的JavaScript庫中的函數,該函數可能會有意無意地拋出一些錯誤。由於我們不能修改這個庫的源代碼,所以大可將對該函數的調用放在try-catch語句當中。萬一有什么錯誤發生,也好恰當地處理它們。
在明明白白地知道自己的代碼會發生錯誤時,再使用try-catch語句就不太合適了。例如,如果傳遞給函數的參數是字符串而非數值,就會造成函數出錯,那么就應該先檢查函數的類型,然后再決定如果去做。在這種情況下,不應該使用try-catch語句。
4,try-catch語句執行順序
看下面的例子:

執行順序為:首先執行try語句塊中的代碼,如果拋出異常,接着執行catch語句塊中代碼,如果沒有異常,catch語句塊中代碼將會被忽略,但不管是否有異常,最后最會執行finally子句。try后面必須接着一個catch或者finally,也就是說JavaScript中的try-catch可以有3中組合形式。即try-catch、try-finally、try-catch-finally三種形式。
try-catch一般的應用場景大家都比較熟悉,下面來看幾個嵌套的例子:

上面這個例子中,最外部的try語句塊中嵌套了一個try-finally語句,內部的try語句中拋出了一個異常,但是內部沒有catch語句塊,所以會執行最近的一個catch語句塊,但是在跳出外部try包含語句塊之前,需要先執行內部的finally語句塊中的代碼,所以最后的結果如上圖所示。再看一個例子:

這個例子中,內部嵌套的語句塊中有catch語句,所以當內部try語句塊中拋出異常時,會接着執行內部的catch語句塊,然后執行finally子句。由於異常已經在內部處理完成,所以外部的catch語句塊會被忽略,所以最終結果如上所示。再看一個例子:

這個例子在上面例子的基礎上,內部的catch語句塊中又拋出了一個異常,所以,在執行完相應語句后,會接着執行外部的catch語句,結果如上所示。
二, 拋出錯誤
與try-catch語句相配的還有一個throw操作符,用於隨時拋出自定義錯誤。拋出錯誤時,必須要給throw操作符指定一個值。這個值是什么類型,沒有要求。下列代碼都是有效的。
throw 12345; throw 'Hello world!'; throw true; throw { name: 'JavaScript'};
在遇到throw操作符時,代碼會立即停止執行。僅當有try-catch語句捕獲到被拋出的值時,代碼才會繼續執行。
通過使用某種內置錯誤類型,可以更真實地模擬瀏覽器錯誤。每種錯誤類型的構造函數接受一個參數,即實際的錯誤信息。下面是一個例子:
throw new Error('Something bad happened.');

這行代碼拋出了一個通用錯誤,帶有一條自定義錯誤信息。瀏覽器會像處理自己生成的錯誤一樣,來處理這行代碼拋出的錯誤。換句話說,瀏覽器會以常規方式報告這一錯誤,並且會顯示這里的自定義錯誤類型。像下面使用其他錯誤類型,也可以模擬出類似的瀏覽器錯誤:
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.");
在創建自定義錯誤消息時,最常用的錯誤類型是Error、RangeError、ReferenceError和TypeError。另外,利用原型鏈還可以通過繼承Error來創建自定義錯誤類型。此時,需要為新創建的錯誤類型指定name和message屬性。來看一個例子:

瀏覽器對待繼承自Error的自定義錯誤類型,就像對待其他錯誤類型一樣。如果要捕獲自己拋出的錯誤並且把它與瀏覽器錯誤區別對待的話,創建自定義錯誤是很有用的。(IE只有在拋出Error對象的時候才會顯示自定義錯誤信息。對於其他類型,它都無一例外地顯示"exception thrown and not caught"(拋出了異常,且未被捕獲))。
