天天看點

JavaScript錯誤處理和堆棧追蹤淺析

有時我們會忽略錯誤處理和堆棧追蹤的一些細節, 但是這些細節對于寫與測試或錯誤處理相關的庫來說是非常有用的. 例如這周, 對于 chai

就有一個非常棒的pr, 該pr極大地改善了我們處理堆棧的方式, 當使用者的斷言失敗的時候, 我們會給予更多的提示資訊(幫助使用者進行定位).

JavaScript錯誤處理和堆棧追蹤淺析

合理地處理堆棧資訊能使你清除無用的資料, 而隻專注于有用的資料. 同時, 當更好地了解 <code>errors</code> 對象及其相關屬性之後, 能有助于你更充分地利用 <code>errors</code>.

<a></a>

在談論錯誤之前, 先要了解下(函數的)調用棧的原理:

當有一個函數被調用的時候, 它就被壓入到堆棧的頂部, 該函數運作完成之後, 又會從堆棧的頂部被移除.

堆棧的資料結構就是後進先出, 以 lifo (last in, first out) 著稱.

例如:

在上述的示例中, 當函數 <code>a</code> 運作時, 其會被添加到堆棧的頂部. 然後, 當函數 <code>b</code> 在函數 <code>a</code> 的内部被調用時, 函數 <code>b</code> 會被壓入到堆棧的頂部. 當函數 <code>c</code> 在函數 <code>b</code> 的内部被調用時也會被壓入到堆棧的頂部.

當函數 <code>c</code> 運作時, 堆棧中就包含了 <code>a</code>, <code>b</code> 和 <code>c</code>(按此順序).

當函數 <code>c</code> 運作完畢之後, 就會從堆棧的頂部被移除, 然後函數調用的控制流就回到函數 <code>b</code>. 函數 <code>b</code> 運作完之後, 也會從堆棧的頂部被移除, 然後函數調用的控制流就回到函數 <code>a</code>. 最後, 函數 <code>a</code> 運作完成之後也會從堆棧的頂部被移除.

為了更好地在demo中示範堆棧的行為, 可以使用 <code>console.trace()</code> 在控制台輸出目前的堆棧資料. 同時, 你要以從上至下的順序閱讀輸出的堆棧資料.

在 node 的 repl 模式中運作上述代碼會得到如下輸出:

正如所看到的, 當從函數 <code>c</code> 中輸出時, 堆棧中包含了函數 <code>a</code>, <code>b</code> 以及<code>c</code>.

如果在函數 <code>c</code> 運作完成之後, 在函數 <code>b</code> 中輸出目前的堆棧資料, 就會看到函數 <code>c</code> 已經從堆棧的頂部被移除, 此時堆棧中僅包括函數 <code>a</code> 和 <code>b</code>.

正如所看到的, 函數 <code>c</code> 運作完成之後, 已經從堆棧的頂部被移除.

當程式運作出現錯誤時, 通常會抛出一個 <code>error</code> 對象. <code>error</code> 對象可以作為使用者自定義錯誤對象繼承的原型.

<code>error.prototype</code> 對象包含如下屬性:

<code>constructor</code>–指向執行個體的構造函數

<code>message</code>–錯誤資訊

<code>name</code>–錯誤的名字(類型)

上述是 <code>error.prototype</code> 的标準屬性, 此外, 不同的運作環境都有其特定的屬性. 在例如 node, firefox, chrome, edge, ie 10+, opera 以及 safari 6+ 這樣的環境中, <code>error</code> 對象具備 <code>stack</code> 屬性, 該屬性包含了錯誤的堆棧軌迹. 一個錯誤執行個體的堆棧軌迹包含了自構造函數之後的所有堆棧結構.

為了抛出一個錯誤, 必須使用 <code>throw</code> 關鍵字. 為了 <code>catch</code> 一個抛出的錯誤, 必須使用 <code>try...catch</code> 包含可能跑出錯誤的代碼. catch的參數是被跑出的錯誤執行個體.

如 java 一樣, javascript 也允許在 <code>try/catch</code> 之後使用 <code>finally</code> 關鍵字. 在處理完錯誤之後, 可以在 <code>finally</code>語句塊作一些清除工作.

在文法上, 你可以使用 <code>try</code> 語句塊而其後不必跟着 <code>catch</code> 語句塊, 但必須跟着 <code>finally</code> 語句塊. 這意味着有三種不同的 <code>try</code> 語句形式:

<code>try...catch</code>

<code>try...finally</code>

<code>try...catch...finally</code>

try語句内還可以在嵌入 <code>try</code> 語句:

也可以在 <code>catch</code> 或 <code>finally</code> 中嵌入 <code>try</code> 語句:

需要重點說明一下的是在抛出錯誤時, 可以隻抛出一個簡單值而不是 <code>error</code> 對象. 盡管這看起來看酷并且是允許的, 但這并不是一個推薦的做法, 尤其是對于一些需要處理他人代碼的庫和架構的開發者, 因為沒有标準可以參考, 也無法得知會從使用者那裡得到什麼. 你不能信任使用者會抛出 <code>error</code> 對象, 因為他們可能不會這麼做, 而是簡單的抛出一個字元串或者數值. 這也意味着很難去處理堆棧資訊和其它元資訊.

如果使用者傳遞給函數 <code>runwithoutthrowing</code> 的參數抛出了一個錯誤對象, 上面的代碼能正常捕獲錯誤. 然後, 如果是抛出一個字元串, 就會碰到一些問題了:

同時, 如果抛出的不是 <code>error</code> 對象, 也就擷取不到 <code>stack</code> 屬性.

errors 也可以被作為其它對象, 你也不必抛出它們, 這也是為什麼大多數回調函數把 errors 作為第一個參數的原因. 例如:

最後, <code>error</code> 對象也可以用于 rejected promise, 這使得很容易處理 rejected promise:

<code>error.capturestacktrace</code> 的第一個參數是 <code>object</code>, 第二個可選參數是一個 <code>function</code>. <code>error.capturestacktrace</code> 會捕獲堆棧資訊, 并在第一個參數中建立 <code>stack</code> 屬性來存儲捕獲到的堆棧資訊. 如果提供了第二個參數, 該函數将作為堆棧調用的終點. 是以, 捕獲到的堆棧資訊将隻顯示該函數調用之前的資訊.

用下面的兩個demo來解釋一下. 第一個, 僅将捕獲到的堆棧資訊存于一個普通的對象之中:

從上面的示例可以看出, 首先調用函數 <code>a</code>(被壓入堆棧), 然後在 <code>a</code> 裡面調用函數 <code>b</code>(被壓入堆棧且在<code>a</code>之上), 然後在 <code>b</code> 中捕獲到目前的堆棧資訊, 并将其存儲到 <code>myobj</code> 中. 是以, 在控制台輸出的堆棧資訊中僅包含了 <code>a</code>和 <code>b</code> 的調用資訊.

現在, 我們給 <code>error.capturestacktrace</code> 傳遞一個函數作為第二個參數, 看下輸出資訊:

當将函數 <code>b</code> 作為第二個參數傳給 <code>error.capturestacktracefunction</code> 時, 輸出的堆棧就隻包含了函數 <code>b</code> 調用之前的資訊(盡管 <code>error.capturestacktracefunction</code> 是在函數 <code>d</code> 中調用的), 這也就是為什麼隻在控制台輸出了 <code>a</code>. 這樣處理方式的好處就是用來隐藏一些與使用者無關的内部實作細節.

來源:51cto