javascript中的錯誤處理機制

小火柴的藍色理想發表於2016-07-17

前面的話

  錯誤處理對於web應用程式開發至關重要,不能提前預測到可能發生的錯誤,不能提前採取恢復策略,可能導致較差的使用者體驗。由於任何javascript錯誤都可能導致網頁無法使用,因此作為開發人員,必須要知道何時可能出錯,為什麼會出錯,以及會出什麼錯。本文將詳細介紹javascript中的錯誤處理機制

 

error物件

  error物件是包含錯誤資訊的物件,是javascript的原生物件。當程式碼解析或執行時發生錯誤,javascript引擎就會自動產生並丟擲一個error物件的例項,然後整個程式就中斷在發生錯誤的地方

console.log(t);//Uncaught ReferenceError: t is not defined

  ECMA-262規定了error物件包括兩個屬性:message和name。message屬性儲存著錯誤資訊,而name屬性儲存錯誤型別

//一般地,使用try-catch語句來捕獲錯誤
try{
    t;
}catch(ex){
    console.log(ex.message);//t is not defined 
    console.log(ex.name);//ReferenceError
}

  瀏覽器還對error物件的屬性做了擴充套件,新增了其他相關資訊。其中各瀏覽器廠商實現最多的是stack屬性,它表示棧跟蹤資訊(safari不支援)

try{
    t;
}catch(ex){
    console.log(ex.stack);//@file:///D:/wamp/www/form.html:12:2
}    

  當然,可以使用error()建構函式來建立錯誤物件。如果指定message引數,則該error物件將把它用做它的message屬性;若不指定,它將使用一個預定義的預設字串作為該屬性的值

new Error();
new Error(message);    
//一般地,使用throw語句來丟擲錯誤
throw new Error('test');//Uncaught Error: test
throw new Error();//Uncaught Error
function UserError(message) {
   this.message = message;
   this.name = "UserError";
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
throw new UserError("errorMessage");//Uncaught UserError: errorMessage

  當不使用new操作符,直接將Error()建構函式像一個函式一樣呼叫時,它的行為和帶new操作符呼叫時一樣

Error();
Error(message);    
throw Error('test');//Uncaught Error: test
throw Error();//Uncaught Error

  error物件有一個toString()方法,返回'Error:'+ error物件的message屬性

var test = new Error('testError');
console.log(test.toString());//'Error: testError'

 

error型別

  執行程式碼期間可能會發生的錯誤有多種型別。每種錯誤都有對應的錯誤型別,而當錯誤發生時,就會丟擲相應型別的錯誤物件。ECMA-262定義了下列7種錯誤型別:

Error
EvalError(eval錯誤)
RangeError(範圍錯誤)
ReferenceError(引用錯誤)
SyntaxError(語法錯誤)
TypeError(型別錯誤)
URIError(URI錯誤)

  其中,Error是基型別,其他錯誤型別都繼承自該型別。因此,所有錯誤型別共享了一組相同的屬性。Error型別的錯誤很少見,如果有也是瀏覽器丟擲的;這個基型別的主要目的是供開發人員丟擲自定義錯誤

【EvalError(eval錯誤)】

  eval函式沒有被正確執行時,會丟擲EvalError錯誤。該錯誤型別已經不再在ES5中出現了,只是為了保證與以前程式碼相容,才繼續保留

【RangeError(範圍錯誤)】

  RangeError型別的錯誤會在一個值超出相應範圍時觸發,主要包括超出陣列長度範圍以及超出數字取值範圍等

new Array(-1);//Uncaught RangeError: Invalid array length
new Array(Number.MAX_VALUE);//Uncaught RangeError: Invalid array length

(1234).toExponential(21);//Uncaught RangeError: toExponential() argument must be between 0 and 20
(1234).toExponential(-1);////Uncaught RangeError: toExponential() argument must be between 0 and 20

【ReferenceError(引用錯誤)】

  引用一個不存在的變數或左值(lvalue)型別錯誤時,會觸發ReferenceError(引用錯誤)

a;//Uncaught ReferenceError: a is not defined
1++;//Uncaught ReferenceError: Invalid left-hand side expression in postfix operation

【SyntaxError(語法錯誤)】

  當不符合語法規則時,會丟擲SyntaxError(語法錯誤)

//變數名錯誤
var 1a;//Uncaught SyntaxError: Unexpected number

// 缺少括號
console.log 'hello');//Uncaught SyntaxError: Unexpected string

【TypeError(型別錯誤)】

  在變數中儲存著意外的型別時,或者在訪問不存在的方法時,都會導致TypeError型別錯誤。錯誤的原因雖然多種多樣,但歸根結底還是由於在執行特定型別的操作時,變數的型別並不符合要求所致

var o = new 10;//Uncaught TypeError: 10 is not a constructor
alert('name' in true);//Uncaught TypeError: Cannot use 'in' operator to search for 'name' in true
Function.prototype.toString.call('name');//Uncaught TypeError: Function.prototype.toString is not generic

【URIError(URI錯誤)】

  URIError是URI相關函式的引數不正確時丟擲的錯誤,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()這六個函式

decodeURI('%2');// URIError: URI malformed

 

error事件

  任何沒有通過try-catch處理的錯誤都會觸發window物件的error事件

  error事件可以接收三個引數:錯誤訊息、錯誤所在的URL和行號。多數情況下,只有錯誤訊息有用,因為URL只是給出了文件的位置,而行號所指的程式碼行既可能出自嵌入的javascript程式碼,也可能出自外部的檔案

  要指定onerror事件處理程式,可以使用DOM0級技術,也可以使用DOM2級事件的標準格式

//DOM0級
window.onerror = function(message,url,line){
    alert(message);
}
//DOM2級
window.addEventListener("error",function(message,url,line){
    alert(message);
});

  瀏覽器是否顯示標準的錯誤訊息,取決於onerror的返回值。如果返回值為false,則在控制檯中顯示錯誤訊息;如果返回值為true,則不顯示

//控制檯顯示錯誤訊息
window.onerror = function(message,url,line){
    alert(message);
    return false;
}
a;

//控制檯不顯示錯誤訊息
window.onerror = function(message,url,line){
    alert(message);
    return true;
}
a;

  這個事件處理程式是避免瀏覽器報告錯誤的最後一道防線。理想情況下,只要可能就不應該使用它。只要能夠適當地使用try-catch語句,就不會有錯誤交給瀏覽器,也就不會觸發error事件

  影象也支援error事件。只要影象的src特性中的URL不能返回可以被識別的影象格式,就會觸發error事件。此時的error事件遵循DOM格式,會返回一個以影象為目標的event物件

  載入影象失敗時會顯示一個警告框。發生error事件時,影象下載過程已經結束,也就是不能再重新下載了

var image = new Image();
image.src = 'smilex.gif';
image.onerror = function(e){
    console.log(e);
}

 

throw語句與丟擲錯誤

  throw語句用於丟擲錯誤。丟擲錯誤時,必須要給throw語句指定一個值,這個值是什麼型別,沒有要求

  [注意]丟擲錯誤的過程是阻塞的,後續程式碼將不會執行

throw 12345;
throw 'hello world';
throw true;
throw {name: 'javascript'};

  可以使用throw語句手動丟擲一個Error物件

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來建立自定義錯誤型別。此時,需要為新建立的錯誤型別指定name和message屬性

  瀏覽器對待繼承自Error的自定義錯誤型別,就像對待其他錯誤型別一樣。如果要捕獲自己丟擲的錯誤並且把它與瀏覽器錯誤區別對待的話,建立自定義錯誤是很有用的

function CustomError(message){
    this.name = 'CustomError';
    this.message = message;
}
CustomError.prototype = new Error();
throw new CustomError('my message');

  在遇到throw語句時,程式碼會立即停止執行。僅當有try-catch語句捕獲到被丟擲的值時,程式碼才會繼續執行

  更詳細的解釋為:當丟擲異常時,javascript直譯器會立即停止當前正在執行的邏輯,並跳轉到就近的異常處理程式。異常處理程式是用try-catch語句的catch從句編寫的。如果丟擲異常的程式碼塊沒有一條相關聯的catch從句,直譯器會檢查更高層的閉合程式碼塊,看它是否有相關聯的異常處理程式。以此類推,直到找到一個異常處理程式為止。如果丟擲異常的函式沒有處理它的try-catch語句,異常將向上傳播到呼叫該函式的程式碼。這樣的話,異常就會沿著javascript方法的詞法結構和呼叫棧向上傳播。如果沒有找到任何異常處理程式,javascript將把異常當成程式錯誤來處理,並報告給使用者

 

try catch語句與捕獲錯誤

  ECMA-262第3版引入了try-catch語句,作為javascript中處理異常的一種標準方式,用於捕獲和處理錯誤

  其中,try從句定義了需要處理的異常所在的程式碼塊。catch從句跟隨在try從句之後,當try塊內某處發生了異常時,呼叫catch內的程式碼邏輯。catch從句後跟隨finally塊,後者中放置清理程式碼,不管try塊中是否產生異常,finally塊內的邏輯總是會執行。儘管catch和finally都是可選的,但try從句需要至少二者之一與之組成完整的語句

  try/catch/finally語句塊都需要使用花括號括起來,這裡的花括號是必需的,即使從句中只有一條語句也不能省略花括號

try{
    //通常來講,這裡的程式碼會從頭到尾而不會產生任何問題
    //但有時會丟擲一個異常,要麼是由throw語句直接丟擲,要麼通過呼叫一個方法間接丟擲
}catch(e){
    //當且僅當try語句塊丟擲了異常,才會執行這裡的程式碼
    //這裡可以通過區域性變數e來獲得對Error物件或者丟擲的其他值的引用
    //這裡的程式碼塊可以基於某種原因處理這個異常,也可以忽略這個異常,還可以通過throw語句重新丟擲異常
}finally{
    //不管try語句是否丟擲了異常,finally裡的邏輯總是會執行,終止try語句塊的方式有:
    //1、正常終止,執行完語句塊的最後一條語句
    //2、通過break、continue或return語句終止
    //3、丟擲一個異常,異常被catch從句捕獲
    //4、丟擲一個異常,異常未被捕獲,繼續向上傳播
}

  一般地,把所有可能會丟擲錯誤的程式碼都放在try語句塊中,而把那些用於錯誤處理的程式碼放在catch塊中

  如果try塊中的任何程式碼發生了錯誤,就會立即退出程式碼執行過程,然後接著執行catch塊。此時,catch塊會接收到一個錯誤資訊的物件,這個物件中包含的實際資訊會因瀏覽器而異,但共同的是有一個儲存著錯誤訊息的message屬性

  [注意]一定要給error物件起個名字,置空會報語法錯誤

try{
    q;
}catch(error){
    alert(error.message);//q is not defined
}

//Uncaught SyntaxError: Unexpected token )
try{
    q;
}catch(){
    alert(error.message);
}

  catch接受一個引數,表示try程式碼塊丟擲的值

function throwIt(exception) {
  try {
    throw exception;
  } catch (e) {
    console.log('Caught: '+ e);
  }
}

throwIt(3);// Caught: 3
throwIt('hello');// Caught: hello
throwIt(new Error('An error happened'));// Caught: Error: An error happened

  catch程式碼塊捕獲錯誤之後,程式不會中斷,會按照正常流程繼續執行下去

try{
  throw "出錯了";
} catch (e) {
  console.log(111);
}
console.log(222);
// 111
// 222

  為了捕捉不同型別的錯誤,catch程式碼塊之中可以加入判斷語句

try {
  foo.bar();
} catch (e) {
  if (e instanceof EvalError) {
    console.log(e.name + ": " + e.message);
  } else if (e instanceof RangeError) {
    console.log(e.name + ": " + e.message);
  }
  // ...
}

  雖然finally子句在try-catch語句中是可選的,但finally子句一經使用,其程式碼無論如何都會執行。換句話說,try語句塊中的程式碼全部正常執行,finally子句會執行;如果因為出錯而執行了catch語句塊,finally子句照樣還會執行。只要程式碼中包含finally子句,則無論try或catch語句塊中包含什麼程式碼——甚至return語句,都不會阻止finally子句的執行

//由於沒有catch語句塊,所以錯誤沒有捕獲。執行finally程式碼塊以後,程式就中斷在錯誤丟擲的地方
function cleansUp() {
  try {
    throw new Error('出錯了……');
    console.log('此行不會執行');
  } finally {
    console.log('完成清理工作');
  }
}
cleansUp();
// 完成清理工作
// Error: 出錯了……
function testFinnally(){
    try{
        return 2;
    }catch(error){
        return 1;
    }finally{
        return 0;
    }
}
testFinnally();//0

  [注意]return語句的count的值,是在finally程式碼塊執行之前,就獲取完成了

var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}
countUp();// 0
console.log(count);// 1
function f() {
  try {
    console.log(0);
    throw "bug";
  } catch(e) {
    console.log(1);
    return true; // 這句原本會延遲到finally程式碼塊結束再執行
    console.log(2); // 不會執行
  } finally {
    console.log(3);
    return false; // 這句會覆蓋掉前面那句return
    console.log(4); // 不會執行
  }
  console.log(5); // 不會執行
}
var result = f();
// 0
// 1
// 3

console.log(result);// false

【tips】塊級作用域

  try-catch語句的一個常見用途是建立塊級作用域,其中宣告的變數僅僅在catch內部有效

  ES6引入了let關鍵字,為其宣告的變數建立塊級作用域。但是,在目前ES3和ES5的情況下,常常使用try-catch語句來實現類似的效果

  由下面程式碼可知,e僅存在於catch分句內部,當試圖從別處引用它時會丟擲錯誤

try{
    throw new Error();//丟擲錯誤
}catch(e){
    console.log(e);//Error(…)
}
console.log(e);//Uncaught ReferenceError: e is not defined

  在IE8-瀏覽器中,catch語句中捕獲的錯誤物件會被新增到執行環境的變數物件,而不是catch語句的變數物件中。換句話說,即使是在catch塊的外部也可以訪問到錯誤物件。IE9修復了這個問題

try{
    a;
}catch(e){
    console.log(1);
}
//在標準瀏覽器中會提示未定義,而在IE8-瀏覽器會顯示錯誤物件
console.log(e);

 

常見錯誤

  錯誤處理的核心是首先要知道程式碼裡會發生什麼錯誤。由於javaScript是鬆散型別的,而且也不會驗證函式的引數,因此錯誤只會在程式碼期間出現。一般來說,需要關注三種錯誤:型別轉換錯誤、資料型別錯誤、通訊錯誤

【型別轉換錯誤】

  型別轉換錯誤發生在使用某個操作符,或者使用其他可能自動轉換值的資料型別的語言結構時

  容易發生型別轉換錯誤的地方是流控制語句。像if之類的語句在確定下一步操作之前,會自動把任何值轉換成布林值。尤其是if語句,如果使用不當,最容易出錯

  未使用過的命名變數會自動被賦予undefined值。而undefined值可以被轉換成布林值false,因此下面這個函式中的if語句實際上只適用於提供了第三個引數的情況。問題在於,並不是只有undefined才會被轉換成false,也不是隻有字串值才可以轉換為true。例如,假設第三個引數是數值0,那麼if語句的測試就會失敗,而對數值1的測試則會通過

function concat(str1,str2,str3){
    var result = str1 + str2;
    if(str3){ //絕對不要這樣
        result += str3;
    }
    return result;
}

  在流控制語句中使用非布林值,是極為常見的一個錯誤來源。為避免此類錯誤,就要做到在條件比較時切實傳入布林值。實際上,執行某種形式的比較就可以達到這個目的

function concat(str1,str2,str3){
    var result = str1 + str2;
    if(typeof str3 == 'string'){ //更合適
        result += str3;
    }
    return result;
}

【資料型別錯誤】

  javascript是鬆散型別的,在使用變數和函式引數之前,不會對它們進行比較以確保它們的資料型別正確。為了保證不會發生資料型別錯誤,只能編寫適當的資料型別檢測程式碼。在將預料之外的值傳遞給函式的情況下,最容易發生資料型別錯誤

//不安全的函式,任何非陣列值都會導致錯誤
function reverseSort(values){
    if(values){
        values.sort();
        values.reverse();
    }
}

  另一個常見的錯誤就是將引數與null值進行比較。與null進行比較只能確保相應的值不是null和undefined。要確保傳入的值有效,僅檢測null值是不夠的

//不安全的函式,任何非陣列值都會導致錯誤
function reverseSort(values){
    if(values != null){
        values.sort();
        values.reverse();
    }
}

  如果傳入一個包含sort()方法的物件(而不是陣列)會通過檢測,但呼叫reverse()函式時可能會出錯

//不安全的函式,任何非陣列值都會導致錯誤
function reverseSort(values){
    if(typeof values.sort == 'function'){
        values.sort();
        values.reverse();
    }
}

  在確切知道應該傳入什麼型別的情況下,最好是使用instanceof來檢測其資料型別

//安全,非陣列值被忽略
function reverseSort(values){
    if(values instanceof Array){
        values.sort();
        values.reverse();
    }
}

【通訊錯誤】

  隨著ajax程式設計的興起,Web應用程式在其生命週期內動態載入資訊或功能,已經成為一件司空見慣的事。不過,javascript與伺服器之間的任何一次通訊,都有可能會產生錯誤

  最常見的問題是在將資料傳送給伺服器之前,沒有使用encodeURIComponent()對資料進行編碼

//錯誤
http://www.yourdomain.com/?redir=http://www.sometherdomain.com?a=b&c=d
//針對'redir='後面的所有字串呼叫encodeURIComponent()就可以解決這個問題
http://www.yourdomain.com/?redir=http:%3A%2F%2Fwww.sometherdomain.com%3Fa%3Db%26c%3Dd

 

參考資料

【1】 ES5/errors https://www.w3.org/html/ig/zh/wiki/ES5/errors
【2】 阮一峰Javascript標準參考教程——錯誤處理機制 http://javascript.ruanyifeng.com/grammar/error.html
【3】《javascript權威指南(第6版)》第三部分 javascript核心參考
【4】《javascript高階程式設計(第3版)》第17章 錯誤處理與除錯

 

相關文章