首頁  >  文章  >  web前端  >  JavaScript中錯誤異常的分析(附範例)

JavaScript中錯誤異常的分析(附範例)

不言
不言轉載
2019-04-04 11:22:422509瀏覽

這篇文章帶給大家的內容是關於JavaScript中錯誤異常的分析(附範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

我的建議是不要隱藏錯誤,勇敢地拋出。沒有人會因為程式碼出現 bug 而導致程式崩潰而羞恥,我們可以讓程式中斷,讓使用者重來。錯誤是無法避免的,如何去處理它才是最重要的。

JavaScript 提供一套錯誤處理機制,錯誤是乾擾程式正常流程的非正常的事故。而沒人可以保持程式沒有 bug,那麼上線後遇到特殊的 bug,如何更快的定位問題所在呢?這就是我們這個專題需要討論的問題。

下面會從JavaScript Error 基礎知識、如何攔截和捕獲異常、如何方便的在線上報錯誤等方面來敘述,本人也是根據網上的知識點進行了一些總結和分析(我只是互聯網的搬運工,不是創造者),如果有什麼錯漏的情況,請在issue 上狠狠的批評我。

這個專題目前是針對瀏覽器的,還沒考慮到 node.js,不過都是 JavaScript Es6 語法,大同小異。

什麼時候 JavaScript 會拋出錯誤呢?

一般分為兩種情況:

JavaScript 自帶錯誤

開發者主動拋出的錯誤

JavaScript 引擎自動拋出的錯誤

大多數場景下我們遇到的錯誤都是這類錯誤。如果發生Javscript 語法錯誤、程式碼參考錯誤、型別錯誤等,JavaScript 引擎就會自動觸發這類錯誤。以下一些場景:

場景一

console.log(a.notExited)
// 浏览器会抛出 Uncaught ReferenceError: a is not defined

場景二

const a;
// 浏览器抛出 Uncaught SyntaxError: Missing initializer in const declaration

語法錯誤,瀏覽器一般第一時間就拋出錯誤,不會等到執行的時候才會報錯誤。

場景三

let data;
data.forEach(v=>{})
// Uncaught TypeError: Cannot read property 'forEach' of undefined

手動拋出的錯誤

一般都是類別庫開發的自訂錯誤例外(如參數等不合法的錯誤異常拋出)。或重新修改錯誤 message 進行上報,以便於理解。

場景一

function sum(a,b){
  if(typeof a !== 'number') {
    throw TypeError('Expected a to be a number.');
  }
  if(typeof b !== 'number') {
    throw TypeError('Expected b to be a number.');
  }
  return a + b;
}
sum(3,'d');
// 浏览器抛出 uncaught TypeError: Expected b to be a number.

場景二

當然我們不一定需要這樣做。

let data;

try {
  data.forEach(v => {});
} catch (error) {
  error.message = 'data 没有定义,data 必须是数组。';
  error.name = 'DataTypeError';
  throw error;
}

如何建立 Error 物件?

建立語法如下:

new Error([message[,fileName,lineNumber]])

省略 new 語法也一樣。

其中fileName 和 lineNumber 不是所有瀏覽器都相容的,Google也不支持,所以可以忽略。

Error 建構子是一般錯誤型,除了 Error 型,還有 TypeError、RangeError 等型別。

Error 實例

這裡列舉的都是 Error 層的原型鏈屬性和方法,更深層的原型鏈的繼承屬性和方便不做說明。一些有相容性的而且不常用的屬性和方法不做說明。

console.log(Error.prototype)
// 浏览器输出 {constructor: ƒ, name: "Error", message: "", toString: ƒ}

其他錯誤型別建構子是繼承 Error,實例是一致的。

屬性

Error.prototype.message

錯誤訊息, Error("msg").message === "msg"。

Error.prototype.name

錯誤類型(名字), Error("msg").name === "Error」。如果是 TypeError,那麼 name 為 TypeError。

Error.prototype.stack

Error 物件作為一個非標準的堆疊屬性提供了一種函數追蹤方式。無論這個函數被調用,處於什麼模式,來自於哪一行或哪個文件,有著什麼樣的參數。這個堆疊產生於最近一次調用最早的那次調用,返回原始的全域作用域調用。

這個不是規範,存在相容性。經測試,Google、火狐、Edge、safar 都支援此特性(都是在最新的版本下測試 2019-04-02),IE 不支援。

方法

Error.prototype.constructor

#Error.prototype.toString

傳回值格式為 ${name}: ${message}。

常用 Error 類型

除了通用的 Error 建構子外,JavaScript還有常見的 5 種其他類型的錯誤建構子。

TypeError

建立一個 Error 實例,表示錯誤的原因:變數或參數不屬於有效類型。

throw TypeError("类型错误");
// Uncaught TypeError: 类型错误

RangeError

建立一個 Error 實例,表示錯誤的原因:數值變數或參數超出其有效範圍。

throw RangeError("数值超出有效范围");
// Uncaught RangeError: 数值超出有效范围

ReferenceError

建立一個 Error 實例,表示錯誤的原因:無效引用。

throw ReferenceError("无效引用");
// Uncaught ReferenceError: 无效引用

SyntaxError

建立一個 Error 實例,表示錯誤的原因:語法錯誤。這種場景很少用,除非類別庫定義了新語法(如範本語法)。

throw SyntaxError("语法错误");
// Uncaught SyntaxError: 语法错误

URIError

建立一個 Error 實例,表示錯誤的原因:涉及 uri 相關的錯誤。

throw URIError("url 不合法");
// Uncaught RangeError: url 不合法

自定义 Error 类型

自定义新的 Error 类型需要继承 Error ,如下自定义 CustomError:

function CustomError(...args){
  class InnerCustomError extends Error {
    name = "CustomError";
  }
  return new InnerCustomError(...args);
}

继承 Error 后,我们只需要对 name 做重写,然后封装成可直接调用的函数即可。

如何拦截 JavaScript 错误?

既然没人能保证 web 应用不会出现 bug,那么出现异常报错时,如何拦截并进行一些操作呢?

try…catch… 拦截

这是拦截 JavaScript 错误,拦截后,如果不手动抛出错误,这个错误将静默处理。平常写代码如果我们知道某段代码可能会出现报错问题,就可以使用这种方式。如下:

const { data } = this.props;
try {
  data.forEach(d=>{});
  // 如果 data 不是数组就会报错
} catch(err){
  console.error(err);
  // 这里可以做上报处理等操作
}

一些使用方式

十分不友好的处理方式

try...catch... 使用需要注意,try…catch… 后,错误会被拦截,如果不主动抛出错误,那么无法知道报错位置。如下面的处理方式就是不好的。

function badHandler(fn) {
  try {
    return fn();
  } catch (err) { /**noop,不做任何处理**/ }
  return null;
}
badHandler();

这样 fn 回调发送错误后,我们无法知道错误是在哪里发生的,因为已经被 try…catch 了,那么如何解决这个问题呢?

相对友好但糟糕的处理方式
function CustomError(...args){
  class InnerCustomError extends Error {
    name = "CustomError";
  }
  return new InnerCustomError(...args);
}
function uglyHandlerImproved(fn) {
  try {
    return fn();
  } catch (err) { 
    throw new CustomError(err.message);
  }
  return null;
}
badHandler();

现在,这个自定义的错误对象包含了原本错误的信息,因此变得更加有用。但是因为再度抛出来,依然是未处理的错误。

try…catch… 可以拦截异步错误吗?

这个也要分场景,也看个人的理解方向,首先理解下面这句话:

try…catch 只会拦截当前执行环境的错误,try 块中的异步已经脱离了当前的执行环境,所以 try…catch… 无效。

setTimeout 和 Promise 都无法通过 try…catch 捕获到错误,指的是 try 包含异步(非当前执行环境),不是异步包含 try(当前执行环境)。异步无效和有效 try…catch 如下:

setTimeout

这个无效:

try {
  setTimeout(() => {
    data.forEach(d => {});
  });
} catch (err) {
  console.log('这里不会运行');
}

下面的 try…catch 才会有效:

setTimeout(() => {
  try {
    data.forEach(d => {});
  } catch (err) {
    console.log('这里会运行');
  }
});

Promise

这个无效:

try {
  new Promise(resolve => {
    data.forEach(d => {});
    resolve();
  });
} catch (err) {
  console.log('这里不会运行');
}

下面的 try…catch 才会有效:

new Promise(resolve => {
  try {
    data.forEach(d => {});
  } catch (err) {
    console.log('这里会运行');
  }
});

小结

不是所有场景都需要 try…catch… 的,如果所有需要的地方都 try…catch,那么代码将变得臃肿,可读性变差,开发效率变低。那么我需要统一获取错误信息呢?有没有更好的处理方式?当然有,后续会提到。

Promise 错误拦截

Promise.prototype.catch 可以达到 try…catch 一样的效果,只要是在 Promise 相关的处理中报错,都会被 catch 到。当然如果你在相关回调函数中 try…catch,然后做了静默提示,那么也是 catch  不到的。

如下会被 catch 到:

new Promise(resolve => {
  data.forEach(v => {});
}).catch(err=>{/*这里会运行*/})

下面的不会被 catch 到:

new Promise(resolve => {
  try {
      data.forEach(v => {});
  }catch(err){}
}).catch(err=>{/*这里不会运行*/})

Promise 错误拦截,这里就不详细说了,如果你看懂了 try…catch,这个也很好理解。

setTimeout 等其他异步错误拦截呢?

目前没有相关的方式直接拦截 setTimeout 等其他异步操作。

如果要拦截 setTimeout 等异步错误,我们需要在异步回调代码中处理,如:

setTimeout(() => {
  try {
    data.forEach(d => {});
  } catch (err) {
    console.log('这里会运行');
  }
});

这样可以拦截到 setTimeout 回调发生的错误,但是如果是下面这样 try…catch 是无效的:

try {
  setTimeout(() => {
    data.forEach(d => {});
  });
} catch (err) {
  console.log('这里不会运行');
}

如何获取 JavaScript 错误信息?

你可以使用上面拦截错误信息的方式获取到错误信息。但是呢,你要每个场景都要去拦截一遍吗?首先我们不确定什么地方会发生错误,然后我们也不可能每个地方都去拦截错误。

不用担心,JavaScript 也考虑到了这一点,提供了一些便捷的获取方式(不是拦截,错误还是会终止程序的运行,除非主动拦截了)。

window.onerror 事件获取错误信息

onerror 事件无论是异步还是非异步错误(除了 Promise 错误),onerror 都能捕获到运行时错误。

需要注意一下几点:

window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx。如果使用 addEventListener,event.preventDefault() 可以达到同样的效果。

window.onerror 是无法捕获到网络异常的错误、