1. 程式人生 > >《編寫可維護的JavaScript》讀書筆記之程式設計實踐-丟擲自定義錯誤

《編寫可維護的JavaScript》讀書筆記之程式設計實踐-丟擲自定義錯誤

丟擲自定義錯誤

在 JavaScript 中丟擲錯誤是一門藝術。一旦理解如何丟擲錯誤,以及在何時丟擲錯誤,除錯程式碼的時間將大大所縮短,對程式碼的滿意度將急劇提升。

錯誤的本質

如果錯誤沒有被丟擲或者報告給開發者,除錯是非常困難的。如果所有的失敗都是悄無聲息,首要的問題是那必將消耗你大量的時間才能發現它,更不要說單獨隔離並修復它了。所以錯誤是開發者的朋友,而非敵人。

【建議】
在程式碼的某個特殊之處計劃一個失敗總比要在所有的地方都預期失敗簡單的多。

【心得】
在函式呼叫鏈的深處丟擲一個錯誤,可以阻止當前函式所在呼叫鏈上的所有上層函式。

function a() { b(); console.log("a"); }
function b() { c(); console.log("b"); }
function c() { throw new Error("執行異常"); }
a();
// 輸出結果
Error:執行異常

在 JavaScript 中丟擲錯誤

在 JavaScript 中丟擲錯誤比在任何其他語言中做同樣的事情更加有價值,這歸咎於 Web 端除錯的複雜性。

throw 操作符:

  • 概述
    throw 操作符能夠將提供的一個物件作為錯誤丟擲。任何型別的物件都可以作為錯誤丟擲。

  • 示例

throw new Error("Something bad happened.");

內建 Error 型別:

  • 概述
    內建的 Error 型別在所有的 JavaScript 實現中都是有效的,它的構造器只接受一個引數,指代錯誤訊息(message)。當以這種方式丟擲錯誤時,如果沒有通過 try-catch 語句來捕獲的話,瀏覽器通常直接顯示該訊息(message 字串)。當今大多數瀏覽器都有一個控制檯,一旦發生錯誤都會在這裡輸出錯誤資訊。換言之,任何你丟擲的和沒丟擲的錯誤都被以相同的方式來對待。

注意:

  1. 開發者可以丟擲任何型別的資料,因為沒有任何規則約束只能丟擲指定的資料型別。但不是所有瀏覽器做出的響應都會按照你的預期。因此,針對所有的瀏覽器,唯一不出差錯的顯示自定義錯誤訊息的方式——丟擲 Error
    物件。
// 不好的寫法
throw "message";
throw { name : 'Nicholas' };
throw true;
throw 12345;
throw new Date();
  1. 如果沒有通過 try-catch 語句捕獲,丟擲任何值都將引發一個錯誤。

丟擲錯誤的好處

丟擲自定義的錯誤可以使用確切的描述文字提供給瀏覽器顯示。除了行和列的號碼,還可以包含任何你需要的有助於除錯問題的資訊。

【推薦】:總是在錯誤訊息中包含函式名稱,以及函式失敗的原因。

function getDivs(element) {
    return element.getElementsByTagName("div");
}
/*
 * 傳遞給函式要操作的 DOM 為 null 可能是很常見的事情,
 * 但實際需要的是 DOM 元素,這會造成錯誤。瀏覽器只會
 * 報一個含糊的錯誤訊息,你得去查詢執行棧,再實際定位
 * 原始檔中的問題。因此丟擲一個錯誤,除錯會更簡單。
 */

function getDivs(element) {
    if(element && element.getElementsByTagName) {
        return element.getElementsByTagName("div");
    } else {
        throw new Error("getDivs(): Argument must be a DOM element.");
    }
}
/*
 * 現在給 getDivs() 函式丟擲一個錯誤,任何時候只要
 * element 不滿足繼續執行的條件,就會丟擲一個錯誤
 * 明確地陳述發生的問題。如果在瀏覽器控制檯中輸出
 * 該錯誤,開發者能馬上開始除錯,並知道最有可能導
 * 致該錯誤的原因。

【傾向】:丟擲錯誤就像給自己留下告訴自己為什麼失敗的便籤。

何時丟擲錯誤

如何丟擲錯誤只是技能水平上的掌握,理解什麼時候丟擲錯誤是思想層次上的要求,這是這一部分要講的內容。

  • 情境
    由於 JavaScript 沒有型別和引數檢查,大量的開發者錯誤地假設他們應該實現每個函式的型別檢查。這種做法並不實際,並且會對指令碼的整體效能造成影響。
  • 示例
// 不好的寫法
function addClass(element, className) {
    if(!element || typeof element.className !== "string") {
        throw new Error("addClass(): First argument must be a DOM element.");
    }
    if(typeof className !== "string") {
        throw new Error("addClass(): Second argument must be a string.");
    }
    element.className += " " + className;
}
/*
 * 這個函式本來只是簡單地給一個給定的元素增加 CSS 類名,
 * 而現在函式的大部分工作變成了錯誤檢查。縱然它能在
 * 每個函式中檢查每個引數,在 JavaScript 中這麼做也
 * 會引起過度的殺傷。
 */

【關鍵】:辨識程式碼中哪些部分在特定的情況下最有可能導致失敗,並只在那些地方丟擲錯誤才是關鍵所在。

  • 總結
  1. 如果一個函式只被已知的實體呼叫,錯誤檢查很可能沒有必要。如果不能提前確定函式會被呼叫的所有地方,你很可能需要一些錯誤檢查。
  2. 丟擲錯誤最佳的地方是在工具函式中,如 addClass() 函式,它是通用指令碼環境中的一部分,會在很多地方使用。更準確的案例是 JavaScript 類庫。
  • 經驗法則
    • 一旦修復了一個很難除錯的錯誤,嘗試增加一兩個自定義錯誤。當再次發生錯誤時,這將有助於更容易解決問題。
    • 如果正在編寫程式碼,思考一下:“我希望【某些事情】不會發生,如果發生,我的程式碼會一團糟糕”。這時,如果“某些事情”發生,就會丟擲一個錯誤。
    • 如果正在編寫的程式碼他人(不知道是誰)也會使用,思考一下他們使用的方式,在特定的情況下丟擲錯誤。

【牢記】:我們的目的不是防止錯誤,而是在錯誤發生時能更加容易地除錯。

try-catch 語句

JavaScript 提供了 try-catch 語句,能在瀏覽器處理丟擲的錯誤前來解析它。

  • try:放置可能引發錯誤的程式碼。

  • catch:放置處理錯誤的程式碼。

  • finally:該塊中的程式碼不管是否有錯誤發生,最後都會被執行。

  • 示例

try {
    somethingThatMightCauseAnError();
} catch(ex) {
    handleError(ex);
} finally {
    continueDoingOtherStuff();
}
/*
 * 當在 try 塊中發生一個錯誤時,程式立刻停止執行,
 * 然後跳到 catch 塊,並傳入一個錯誤物件。
 */
  • 注意
    在某些情況下,finally 塊工作起來有點複雜。例如,如果 try 塊中包含一個 return 語句,實際上它必須等到 finally 塊中的程式碼執行後才能返回。

使用 throw 還是 try-catch:

通常開發者很難敏銳地判斷是丟擲一個錯誤還是用 try-catch 來捕獲一個錯誤。

【建議】

  1. 錯誤只應該在應用程式棧中最深的部分丟擲。任何處理應用程式特定邏輯的程式碼都應該有錯誤處理的能力,並且捕獲從底層元件中丟擲的錯誤。
  2. 應用程式邏輯總是呼叫某個特定函式的原因,因此也是最適合處理錯誤的。
  3. 千萬不要將 try-catch 中的 catch 塊留空,你應該總是寫點什麼來處理錯誤。

錯誤型別

ECMA-262 規範指出了 7 中錯誤型別。當不同錯誤條件發生時,這些型別在 JavaScript 引擎中都有用到,當然我們也可以手動建立它們。

  • Error:所有錯誤的基本型別。但實際上引擎從來不會丟擲該型別的錯誤。
  • EvalError:通過 eval() 函式執行程式碼發生錯誤時丟擲。
  • RangeError:超出邊界時丟擲錯誤。
  • ReferencceError:引用不存在時丟擲錯誤。
  • SyntaxError:給 eval() 函式傳遞的程式碼中有語法錯誤時丟擲。
  • TypeError:變數不是期望的型別時丟擲。
  • URIError:給 encodeURI()、encodeURIComponent()、decodeURI()、decodeURIComponent() 等函式傳遞格式非法的 URI 字串時丟擲。

【建議】

  • 所有的錯誤型別都繼承自 Error,所以用 instanceof Error 檢查其型別得不到任何有用的資訊。通過檢查特定的錯誤型別可以更可靠地處理錯誤。
try {
    // 有些程式碼引發了錯誤
} catch(ex) {
    if(ex instanceof TypeError) {
        // 處理 TypeError 錯誤
    } else if(ex instanceof ReferenceError) {
        // 處理 ReferenceError 錯誤
    } else {
        // 其他處理
    }
}
  • 建立自己的錯誤型別,讓其繼承自 Error。這種做法允許你提供額外的資訊,同時區別於瀏覽器丟擲的錯誤。
function MyError(message) {
    this.message = message;
}
MyError.prototype = new Error();
/*
 * 該方法在 IE8 以及早期瀏覽器中不顯示錯誤資訊。
 * 但該方法最大的好處是,自定義錯誤型別可以檢測
 * 自己的錯誤。
 */

try {
    // 有些程式碼引發了錯誤
} catch(ex) {
    if(ex instanceof MyError) {
        // 處理自己的錯誤
    } else {
        // 其他處理
    }
}