《javascript高階程式設計》學習筆記 | 21.2.錯誤處理

小讴發表於2024-02-28
關注前端小謳,閱讀更多原創技術文章

錯誤處理

相關程式碼 →

try/catch 語句

  • ES3 新增了try/catch語句,基本語法與 Java 中的 try/catch 一樣
try {
  // 可能出錯的程式碼
  const a = 3;
  a = 4;
} catch (error) {
  // 出錯時執行的程式碼
  console.log("An error happened!"); // An error happened!
}
  • try 塊中有程式碼發生錯誤,程式碼會立即退出執行並跳到 catch 塊中
  • 所有瀏覽器都支援錯誤物件的messagename屬性
try {
  const a = 3;
  a = 4;
} catch (error) {
  console.log(error);
  /* 
    TypeError: Assignment to constant variable.
      at Object.<anonymous> (c:\Users\43577\Desktop\工作\my_project\my_demos\javascript高階程式設計(第四版)\第21章 錯誤處理與除錯\21.2.錯誤處理.js:13:5)
      at Module._compile (internal/modules/cjs/loader.js:1085:14)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
      at Module.load (internal/modules/cjs/loader.js:950:32)
      at Function.Module._load (internal/modules/cjs/loader.js:790:12)
      at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:75:12)
      at internal/main/run_main_module.js:17:47
  */
  console.log(error.name); // TypeError(型別錯誤)
  console.log(error.message); // Assignment to constant variable.(常量被賦值)
}

finally 子句

  • try/catch 中可選的 finally 子句始終執行,二者均無法阻止finally 塊執行

    • try 中程式碼執行完,會執行 finally 的程式碼
    • 出錯並執行 catch 中程式碼,仍會執行 finally 的程式碼
// finally 子句
try {
  console.log(1); // 1,執行
} catch (error) {
  console.log(2); // 不執行
} finally {
  console.log(3); // 3,執行
}

try {
  const a = 3;
  a = 4;
} catch (error) {
  console.log(2); // 2,try出錯執行catch
} finally {
  console.log(3); // 3,仍執行
}
  • 程式碼中包含 finally, try 或 catch 中的 return 會被忽略
console.log(
  (function testFinally() {
    try {
      console.log("try"); // try,非return語句不受影響
      return 1;
    } catch (error) {
      return 2;
    } finally {
      console.log("finally"); // finally
      return 3;
    }
  })()
); // 3,包含finally語句,try或catch中的return會被忽略

錯誤型別

  • Error,基型別
  • InternalError,底層引擎異常時,如遞迴過多導致的棧溢位
  • EvalError,使用 eval()異常時,但瀏覽器不會總丟擲 EvalError
  • RangeError,數值越界時
  • ReferenceError,找不到物件時
  • SyntaxError,給 eval()傳入的字串包含語法錯誤時
  • TypeError,最常見

    • 變數不是預期型別時
    • 訪問不存在的方法時
new Array(-1); // RangeError: Invalid array length
let obj = x; // ReferenceError: x is not defined
eval("1++2"); // SyntaxError: Invalid left-hand side expression in postfix operation
console.log("a" in "abc"); // TypeError: Cannot use 'in' operator to search for 'a' in abc
Function.prototype.toString().call("name"); // TypeError: Function.prototype.toString(...).call is not a function
  • 可以使用instanceof運算子在 catch 塊中確定錯誤型別
try {
  const a = 3;
  a = 4;
} catch (error) {
  if (error instanceof TypeError) {
    console.log("TypeError!");
  }
} // TypeError!
try {
  new Array(-1);
} catch (error) {
  if (error instanceof RangeError) {
    console.log("RangeError!");
  }
} // RangeError!

try/catch 的用法

  • 瀏覽器認為 try/catch 中發生的錯誤已被處理,不會再報錯
  • try/catch 最好使用在開發者無法控制有可能出現錯誤上(如不便修改程式碼的第三方 js 庫,最好使用 try/catch 把函式呼叫包起來)

丟擲錯誤

  • throw 運算子可在任何時候丟擲自定義錯誤

    • throw 運算子必須有值,型別不限
    // throw 12345; // Uncaught 12345,後續程式碼停止
    // throw "Hello world"; // Uncaught Hello world,後續程式碼停止
    // throw true; // Uncaught true,後續程式碼停止
    // throw { name: "JS" }; // Uncaught {name: 'JS'},後續程式碼停止
    • 使用 throw 時程式碼立即停止,try/catch 語句中捕獲了丟擲的值時除外
    try {
      throw 123;
    } catch (error) {
      console.log(123);
    } // 123
    console.log(5); // 5,throw被try/catch捕獲,後續程式碼照常
  • 可透過內建錯誤型別模擬瀏覽器錯誤
// throw new SyntaxError; //  Uncaught SyntaxError
// throw new InternalError; //  Uncaught InternalError
// throw new TypeError; //  Uncaught TypeError
// throw new RangeError; //  Uncaught RangeError
// throw new EvalError; //  Uncaught EvalError
// throw new URIError; //  Uncaught URIError
// throw new RefenceError; //  Uncaught RefenceError
  • 可透過繼承 Error建立自定義錯誤型別,建立時需提供 name 和 message 屬性
class CustomError extends Error {
  constructor(message) {
    super(message); // super呼叫父類建構函式,手動給父類傳參,並將返回值賦給子類中的this
    this.name = "CustomError";
    this.message = message;
  }
}
// throw new CustomError("My message"); // CustomError: My message

何時丟擲錯誤

  • 已知函式無法正確執行時,瀏覽器會自動丟擲錯誤
  • 複雜的程式很難找到錯誤原因,適當建立自定義錯誤可有效提高程式碼的可維護性
  • 應仔細評估每個函式,尤其可能導致失敗的情形
function process(values) {
  if (!(values instanceof Array)) {
    throw new Error("process(): Argument must be an Array.");
  }
  values.sort(); // 如果values不是陣列,則瀏覽器會報錯。因此在此句之前判斷引數型別且用自定義錯誤,可有效提高程式碼可維護性
  for (let value of values) {
    if (value > 100) {
      return value;
    }
  }
  return -1;
}
// process(1); // Error: process(): Argument must be an Array.
// process(1); // TypeError: values.sort is not a function(如果沒有throw程式碼段的結果)

丟擲錯誤與 try/catch

  • 捕獲錯誤的目的是阻止瀏覽器以其預設方式響應
  • 丟擲錯誤的目的是提供有關其發生原因的說明
  • 應該在明確接下來做什麼時捕獲錯誤

error 事件

  • 沒有被 try/catch 捕獲的錯誤會在瀏覽器 window 物件上觸發 error 事件

    • onerror 事件處理程式中,任何瀏覽器都不傳入 event 物件
    • 傳入 3 個引數:錯誤訊息、發生錯誤的 URL、發生錯誤的行號
    • 任何錯誤發生都會觸發 error 事件,並執行事件的處理程式,瀏覽器預設行為會生效
    • 可以返回 false 來阻止瀏覽器預設報告錯誤的行為
window.onerror = (message, url, line) => {
  console.log(message);
  return false; // 阻止瀏覽器預設報告錯誤
};
  • 圖片中 src 屬性的 url 沒有返回可識別的圖片格式,也會觸發 error 事件
const image = new Image();
image.addEventListener("load", (event) => {
  console.log("Image loaded!");
});
image.addEventListener("error", (event) => {
  console.log("Image not loaded!");
});
image.src = "a.jpg"; // Image not loaded!

識別錯誤

型別轉換錯誤

  • 主要原因是使用了會自動改變某個值的資料型別的草錯付或語言構造

    • 比較過程中,應使用嚴格相等嚴格不等避免錯誤
    console.log(5 == "5"); // true
    console.log(5 === "5"); // false,資料型別不同
    console.log(1 == true); // true
    console.log(1 === true); // false,資料型別不同
    • 在 if、for、while 等流程控制語句中,應堅持使用布林值作為條件避免錯誤
    function concat(str1, str2, str3) {
      let result = str1 + str2;
      if (str3) {
        result += str3;
      }
      return result;
    }
    console.log(concat("1", "2", "0")); // '120'
    console.log(concat("1", "2")); // '12',str3是undifined,轉化為false
    console.log(concat("1", "2", 0)); // '12',str3是數值0,轉化為false,與預期不符
    
    function concat(str1, str2, str3) {
      let result = str1 + str2;
      if (str3 !== undefined) {
        result += str3;
      }
      return result;
    }
    console.log(concat("1", "2", "03")); // '120'
    console.log(concat("1", "2")); // '12',str3 是 undifined,轉化為 false
    console.log(concat("1", "2", 0)); // '120',達到預期

資料型別錯誤

  • JS 是鬆散型別,其變數函式引數不能保證資料型別

    • 原始型別的值,使用typeof檢測
    function getQueryString(url) {
      const pos = url.indexOf("?"); // indexOf是字串才有的方法
      if (pos > 1) {
        console.log(url.substring(pos + 1)); // substring是字串才有的方法
        return;
      }
      console.log("not has ?");
    }
    // getQueryString(123); // TypeError: url.indexOf is not a function
    
    function getQueryString2(url) {
      if (typeof url === "string") {
        // 確保不會因為引數是非字串值而報錯
        const pos = url.indexOf("?");
        if (pos > 1) {
          console.log(url.substring(pos + 1));
          return;
        }
        console.log("not has ?");
      }
    }
    getQueryString2(123); // 不列印
    getQueryString2("123"); // 'not has ?'
    getQueryString2("https://www.baidu.com?keyWord=error"); // 'keyWord=error'
    • 物件值,使用instanceof檢測
    function reverseSort(values) {
      if (values) {
        // 不可取,values為true的情況很多
        values.sort();
        values.reverse();
      }
    }
    // reverseSort(1); // TypeError: values.sort is not a function
    
    function reverseSort2(values) {
      if (values !== null) {
        // 不可取,values不為null的情況很多
        values.sort();
        values.reverse();
      }
    }
    // reverseSort2(1); // TypeError: values.sort is not a function
    
    function reverseSort3(values) {
      if (typeof values.sort === "function") {
        // 不可取,假如values有sort()方法但不是陣列則會報錯
        values.sort();
        values.reverse();
      }
    }
    // reverseSort3({
    //   sort: () => {
    //     console.log("3");
    //   },
    // }); // 先values.sort()列印3,後報錯TypeError: values.reverse is not a function
    
    function reverseSort4(values) {
      if (values instanceof Array) {
        // 可取,確保values是Array的例項
        values.sort();
        values.reverse();
      }
    }
    let val1 = 1;
    let val2 = [1, 2];
    reverseSort4(val1);
    reverseSort4(val2);
    console.log(val1); // 1
    console.log(val2); // [2,1]

通訊錯誤

  • 對於 url 的查詢字串,都要透過encodeURIComponent(),以確保編碼合適
let url = "https://www.baidu.com?keyWord=https://www.taobao.com"; // url格式不正確
function addQueryStringArg(url, name, value) {
  if (url.indexOf("?") === -1) {
    url += "?";
  } else {
    url += "&";
  }
  url += `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
  return url;
}
let url2 = addQueryStringArg(
  "https://www.baidu.com",
  "keyWord",
  "https://www.taobao.com"
);
console.log(url2); // https://www.baidu.com?keyWord=https%3A%2F%2Fwww.taobao.com,與伺服器通訊的正確url格式

區分重大與非重大錯誤

  • 非重大錯誤

    • 不影響使用者主要任務
    • 隻影響頁面中某個部分
    • 可恢復
    • 重複操作可能成功
  • 重大錯誤

    • 程式無法繼續執行
    • 嚴重影響使用者的主要目標
    • 會導致其他錯誤
  • 程式某個部分的錯誤,不應該影響其他部分
  • 模組初始化時,可在 for 迴圈中加入 try/catch 語句,避免某一模組初始化時發生錯誤影響其他模組
let mods = [
  {
    name: "mod1",
    init: () => {
      const a = 1;
      a = 2;
      console.log("mod1 init");
    }, // mod1的init方法裡有錯誤
  },
  {
    name: "mod2",
    init: () => {
      console.log("mod2 init");
    },
  },
];
for (let mod of mods) {
  // mod.init(); // 不好,只要有一個mod的init方法出錯,影響後續
  try {
    mod.init(); // 'mod2 init',mod2照常執行
  } catch (error) {
    console.log(error); // TypeError: Assignment to constant variable.
  }
}

總結 & 問點

  • 錯誤物件中的哪些屬性在全部瀏覽器中都向使用者顯示?
  • finally 對 try 或 catch 中的非 return 語句和 return 語句分別有什麼影響?
  • 請舉例說明有哪些常見錯誤型別及其出現的原因
  • 請寫一段程式碼,在 try/catch 塊中,確定錯誤的型別
  • throw 運算子必須有值嘛?需要什麼資料型別的值?如何才能既使用該運算子又不影響後續程式碼執行?
  • 寫一段程式碼,透過繼承 Error 建立一個自定義的錯誤型別,建立其例項並用 throw 將其丟擲
  • 寫一段程式碼,在一個函式里,透過建立自定義錯誤型別提高其可維護性
  • 常見的型別轉換錯誤有哪些?分別該如何避免呢?
  • 應分別怎樣檢測,以避免原始值和物件值在函式傳參時可能發生的資料型別錯誤?
  • 寫一個方法,處理與伺服器通訊的 url 的正確格式
  • 寫一段程式碼,用 for 迴圈模擬模組初始化,某一模組載入時發生錯誤但不影響後續模組

相關文章