JS 中原生錯誤類型總結

Hi 你們好,我是張小豬。此次咱們來聊聊 JS 中的那些原生錯誤類型。git

初衷

可能會有小夥伴好奇,爲何小豬要寫這麼一篇文章呢?這件事情說來話長了。github

小豬的從業時間並不長,四捨五入也就剛畢業(哈哈哈,永遠 22 歲)。不過坦白說,以前在一些不一樣的地方,小豬時常見到一些明明能夠給出更明確的錯誤類型,不過都無論三七二十一統統 throw new Error("xxxx") 或者 throw "xxxx" 這樣的代碼。就像江南皮革廠的那些統統 20 塊同樣,印象深入的刻進了 DNA 裏(不要什麼奇怪的東西都往 DNA 裏刻啊喂shell

並且,這也讓我想到了另一個時不時就碰見的情況,那就是無論四七二十八,全部 API 統統返回 200 OK。反正先 200 了再說,而後 body 裏再給個鑑權失敗的消息。什麼?還有除了 200 之外的狀態碼?不存在的!json

每次遇到這樣的 API,小豬都是黑人問號臉...EXM?王德發?不過那都是另一個故事了。咱們仍是說回今天的主題吧,JS 中原生的錯誤類型。segmentfault

原生錯誤類型

如上文提到,在 JS 中存在着一個全局對象 Error。因爲它是繼承自 Function,因此小夥伴們的那些對於函數的騷操做均可以對它使用。不過它更常見的仍是用做構造函數,產生錯誤實例,例如:瀏覽器

try {
  throw new Error('小豬纔是最萌噠!')
} catch (e) {
  console.error('本題正解:' + e.message)
}

在錯誤實例中,咱們常常訪問的通用屬性有 namemessage,其中 name 表示這個錯誤類型的名字,而 message 則是錯誤信息。除此以外,還有諸如 fileNamelineNumbercolumnNumberstack 這些常見的也很容易理解的屬性。還有就是一些,不一樣瀏覽器本身實現的屬性,這裏就不作過多的介紹啦。下面舉個簡單的栗子:函數

try {
  throw new Error('你猜')
} catch (e) {
  console.error(`錯誤類型名稱:${e.name} 錯誤信息:${e.message}`)
}

最後須要說的一點是,咱們接下來要介紹的幾種原生的錯誤類型,其實都是來自於 Error 這個基類。那麼,咱們就按照字母序來逐個說明吧。測試

EvalError

一上來就遇到一個不常見的錯誤類型。不過不用擔憂,其實咱們能夠忽略這個類型。優化

EvalError 這個錯誤類型原本的做用是用來表示在全局 eval 函數中產生的錯誤,不過如今的 JS 引擎都不會再拋出這個錯誤了。在 ECMAScript 規範裏,也說明了保留這個對象只是爲了向前兼容而已。this

RangeError

RangeError 這個錯誤類型自己是表示一個值超出了它能夠被容許的範圍。這個值有多是一個枚舉值,超出了集合範圍;也有多是一個數值,超出了有效範圍。這裏針對以上兩種狀況分別舉一個栗子:

try {
  String.prototype.normalize.call('\u0061', 'HELLO')
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // RangeError: The normalization form should be one of NFC, NFD, NFKC, NFKD.
}

try {
  const arr = new Uint8Array(233 ** 33)
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // RangeError: Invalid typed array length: 1.3266164977310088e+78
}

固然,咱們也能夠根據實際需求,自行拋出這個錯誤。例如這裏例子中用一個 checkNum 方法來檢測數字是否在咱們須要的範圍內:

function checkNum(num) {
  if (num < -500 || num > 500) {
    throw new RangeError("The argument must be between -500 and 500.")
  }
  // Your logic
}

try {
  check(2000)
} catch(e) {
  if (e instanceof RangeError) {
    // Handle the error
  }
}

ReferenceError

ReferenceError 這個錯誤類型自己表示檢測到了一個無效的引用,最多見的狀況就是嘗試訪問一個不存在的變量。例如:

try {
  const value = undefinedVariable
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // ReferenceError: undefinedVariable is not defined
}

不過這裏須要注意的是,一個變量的值是 undefined 與這個變量不存在是不同的。咱們能夠看下面這個栗子:

try {
  const undefinedVariable = undefined;
  const value = undefinedVariable;
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // Won't occur
}

這裏背後具體的緣由會和咱們 JS 標準中的兩個概念有關,一個是執行上下文(Execution Context),一個是環境記錄(Environment Record)。這裏小豬並不打算展開來講這兩個概念,只是先作一個簡單的比喻便於理解。

function intro() {
  const name = '張小豬'
  console.log(name)
}
intro()

當咱們執行上面這樣的代碼的時候,咱們能夠想象爲在 intro 函數裏面有一個咱們看不見的對象,它用鍵值對的形式記錄着咱們申明的變量。當前它的狀態多是:

{
  "name": "張小豬"
}

因此咱們接下來訪問 name 變量的時候能夠正常的找到它的值。而後咱們把代碼再變爲下面這樣:

function intro() {
  const name = '張小豬'
  const age = undefined
  console.log(name, age)
  console.log(hobbies)
}
try {
  intro()
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
}

這時候咱們的這個看不見的對象能夠想象爲:

{
  "name": "張小豬",
  "age": undefined
}

因爲並無一個名叫 hobbies 的鍵,因此咱們會獲得下面這個結果:

張小豬 undefined
ReferenceError: hobbies is not defined

SyntaxError

SyntaxError 這個錯誤類型自己表示 JS 引擎在處理代碼的時候碰見了非法的 JS 代碼。爲了更容易的可以明白這個場景,咱們先簡單的描述一下 JS 引擎的解析處理過程吧。

大致上看,這個過程能夠分紅 3 個部分:

  1. 詞法分析
  2. 語法分析
  3. 解釋執行

詞法分析

其中第一個過程 - 詞法分析,簡單的說就是把咱們輸入的代碼字符串轉換爲一個個的 token,獲得的結果以供下一步語法分析使用。這裏一個個的 token 能夠理解爲就是一個個的小單元,咱們仍是結合一個具體的栗子吧。

例如對於 a = (2 + b 這個字符串,咱們能夠獲得這樣一些小單元:

  • 標識符 a
  • 賦值運算符
  • 開括號
  • 數字 2
  • 加法運算符
  • 標識符 b

這裏其實能夠注意到,咱們若是人爲的看這串代碼,會明顯的發現缺乏了閉合括號。不過在進行詞法分析的時候,只是按照規則拆分和標記 token,並不會作語法相關的處理。

語法分析

第二個過程 - 語法分析,簡單的說就是基於上一步獲得的 token,按照 JS 的語法規則進行判斷和分析,並生成抽象語法樹(AST)之類的東西。若是看回上面的那個栗子的話,因爲缺乏閉合括號,因此是不符合 JS 的語法規範的。這時候 JS 引擎就會拋出一個 SyntaxError 的實例了。至於什麼是 AST,咱們等到之後的相關專題再來聊吧,這裏就不展開啦。

解釋執行

這個過程簡單的理解就是讓咱們的代碼跑起來。不過因爲 JS 是解釋型語言,因此這一步會須要藉助解釋器來不停的解釋咱們的代碼,並翻譯成字節碼以供後續執行。固然這中間還存在着比較複雜的過程並伴隨着大量的優化。

報錯舉例

簡單介紹完這 3 個過程後,咱們能夠發現,SyntaxError 一般會來自於前兩個過程。那麼咱們仍是舉幾個報錯的栗子吧:

try {
  const a b = 3
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // Uncaught SyntaxError: Missing initializer in const declaration
}
try {
  const a = 1 * % 2
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // Uncaught SyntaxError: Unexpected token '%'
}

TypeError

TypeError 這個錯誤類型自己表示由於一個變量或者參數並非合法的或者符合指望的類型,從而致使咱們的操做行爲沒法成功的完成。一般可能會有如下幾種狀況:

  • 咱們給一個函數或者一個操做符傳遞了一個不符合預期類型的值
  • 嘗試修改一個不可更改的變量
  • 嘗試對一個值作一些不符合它類型的事情

這裏仍是舉幾個栗子更直觀一點:

try {
  const a = 1
  a = 2
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // TypeError: Assignment to constant variable.
}
try {
  const a = 1;
  a();
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // TypeError: a is not a function
}

URIError

URIError 這個錯誤類型自己表示全局的 URI 處理函數在使用中產生了問題。例如當咱們傳入了一些非法參數的時候,就會拋出這個錯誤。這裏列舉幾個咱們經常使用的全局 URI 處理函數:

  • encodeURI
  • decodeURI
  • encodeURIComponent
  • decodeURIComponent

一樣咱們舉個會報錯的栗子:

try {
  decodeURI('abc%abc')
} catch (e) {
  console.error(`${e.name}: ${e.message}`)
  // URIError: URI malformed
}

自定義錯誤類型

上面我介紹了各類原生的錯誤類型它們自己的含義。固然,在具體開發的時候,咱們也能夠根據自行的實際需求拋出這些錯誤。不過它們終究仍是沒法表明全部的錯誤,難道對於其它的錯誤咱們都只能夠用基礎的 Error 類型了麼?

固然不是的,咱們其實能夠按照自身的需求建立一些自定義的錯誤類型。這裏咱們就基於 ES2015+ 的 class 語法來作一個實現吧:

class SoundError extends Error {
  constructor(yell = 'bark', vol = 10, ...params) {
    super(...params);
    this.name = 'SoundError';
    this.voice = yell;
    this.volume = vol;
  }
}

try {
  throw new SoundError('嚶嚶嚶', 1, '小拳拳錘你胸口');
} catch (e) {
  console.error(`${e.name}: ${e.voice} at volume ${e.volume} since ${e.message}`);
  // SoundError: 嚶嚶嚶 at volume 1 since 小拳拳錘你胸口
}

應用

錯誤信息的應用場景,天然是須要拋出錯誤的地方啦(不要打我...

不太小豬我的感受是,對於須要給其餘人使用或者維護的代碼,若是咱們能把報錯信息作的比較清楚易懂,對於使用者和維護者都是很舒服的事情。除了這種體驗上的感覺外,在另一些具體的場景,小豬也常用到各類錯誤類型。

首先是,自己在咱們本身的代碼中,涉及到捕獲錯誤的地方,就能夠根據錯誤類型來作針對的邏輯處理。

再好比,自動化測試。一方面是,在測試用例中,咱們可能須要根據不一樣的錯誤類型進行鍼對的判斷和處理;另外一方面,咱們也能夠方便的維護一份公用的錯誤信息,用以同時給代碼中的斷言、測試用例中的判斷等須要的地方來使用。

又或者,咱們若是作一些錯誤上報收集之類的事情,也能夠根據不一樣錯誤的類型進行對應的數據獲取、序列化與反序列化、展現等等。

總結

上面依次介紹了 JS 中的原生錯誤類型,以及咱們如何自定義錯誤類型。其中 ReferenceErrorSyntaxError 稍微作了一點小擴展。

最後,小小的說了一下應用場景。固然,我也只是舉了幾個栗子,核心仍是那句話,須要拋出錯誤的地方,儘可能拋出相對準確和清晰的錯誤就好啦。

但願能幫助到有須要的小夥伴。若是你以爲不錯的話,不要忘記三連支持小豬哦。小豬愛大家喲~

相關連接

qrcode_green.jpeg

相關文章
相關標籤/搜索