摘要:當執行完畢后也會從棧頂移出,控制流交還到。一個的堆棧追蹤包含了從其構造函數開始的所有堆棧幀。我們將捕獲當前堆棧路徑并且將其存儲到一個普通對象中。用表示起始堆棧函數指示器這個名字記錄。
譯者注:本文作者是著名 JavaScript BDD 測試框架 Chai.js 源碼貢獻者之一,Chai.js 中會遇到很多異常處理的情況。跟隨作者思路,從 JavaScript 基本的 Errors 原理,到如何實際使用 Stack Traces,深入學習和理解 JavaScript Errors 和 Stack Traces。文章貼出的源碼鏈接也非常值得學習。
作者:lucasfcosta
編譯:胡子大哈
翻譯原文:[http://huziketang.com/blog/po...
](http://huziketang.com/blog/po...
英文原文:JavaScript Errors and Stack Traces in Depth
轉載請注明出處,保留原文鏈接以及作者信息
很久沒給大家更新關于 JavaScript 的內容了,這篇文章我們來聊聊 JavaScript 。
這次我們聊聊 Errors 和 Stack traces 以及如何熟練地使用它們。
很多同學并不重視這些細節,但是這些知識在你寫 Testing 和 Error 相關的 lib 的時候是非常有用的。使用 Stack traces 可以清理無用的數據,讓你關注真正重要的問題。同時,你真正理解 Errors 和它們的屬性到底是什么的時候,你將會更有信心的使用它們。
這篇文章在開始的時候看起來比較簡單,但當你熟練運用 Stack trace 以后則會感到非常復雜。所以在看難的章節之前,請確保你理解了前面的內容。
Stack是如何工作的在我們談到 Errors 之前,我們必須理解 Stack 是如何工作的。它其實非常簡單,但是在開始之前了解它也是非常必要的。如果你已經知道了這些,可以略過這一章節。
每當有一個函數調用,就會將其壓入棧頂。在調用結束的時候再將其從棧頂移出。
這種有趣的數據結構叫做“最后一個進入的,將會第一個出去”。這就是廣為所知的 LIFO(后進先出)。
舉個例子,在函數 x 的內部調用了函數 y,這時棧中就有個順序先 x 后 y。我再舉另外一個例子,看下面代碼:
function c() { console.log("c"); } function b() { console.log("b"); c(); } function a() { console.log("a"); b(); } a();
上面的這段代碼,當運行 a 的時候,它會被壓到棧頂。然后,當 b 在 a 中被調用的時候,它會被繼續壓入棧頂,當 c 在 b 中被調用的時候,也一樣。
在運行 c 的時候,棧中包含了 a,b,c,并且其順序也是 a,b,c。
當 c 調用完畢時,它會被從棧頂移出,隨后控制流回到 b。當 b 執行完畢后也會從棧頂移出,控制流交還到 a。最后,當 a 執行完畢后也會從棧中移出。
為了更好的展示這樣一種行為,我們用console.trace()來將 Stack trace 打印到控制臺上來。通常我們讀 Stack traces 信息的時候是從上往下讀的。
function c() { console.log("c"); console.trace(); } function b() { console.log("b"); c(); } function a() { console.log("a"); b(); } a();
當我們在Node REPL服務端執行的時候,會返回如下:
Trace at c (repl:3:9) at b (repl:3:1) at a (repl:3:1) at repl:1:1 // <-- For now feel free to ignore anything below this point, these are Node"s internals at realRunInThisContextScript (vm.js:22:35) at sigintHandlersWrap (vm.js:98:12) at ContextifyScript.Script.runInThisContext (vm.js:24:12) at REPLServer.defaultEval (repl.js:313:29) at bound (domain.js:280:14) at REPLServer.runBound [as eval] (domain.js:293:12)
從上面我們可以看到,當棧信息從 c 中打印出來的時候,我看到了 a,b 和 c。現在,如果在 c 執行完畢以后,在 b 中把 Stack trace 打印出來,我們可以看到 c 已經從棧中移出了,棧中只有 a 和 b。
function c() { console.log("c"); } function b() { console.log("b"); c(); console.trace(); } function a() { console.log("a"); b(); } a();
下面可以看到,c 已經不在棧中了,在其執行完以后,從棧中 pop 出去了。
Trace at b (repl:4:9) at a (repl:3:1) at repl:1:1 // <-- For now feel free to ignore anything below this point, these are Node"s internals at realRunInThisContextScript (vm.js:22:35) at sigintHandlersWrap (vm.js:98:12) at ContextifyScript.Script.runInThisContext (vm.js:24:12) at REPLServer.defaultEval (repl.js:313:29) at bound (domain.js:280:14) at REPLServer.runBound [as eval] (domain.js:293:12) at REPLServer.onLine (repl.js:513:10)
概括一下:當調用時,壓入棧頂。當它執行完畢時,被彈出棧,就是這么簡單。
Error 對象和 Error 處理當Error發生的時候,通常會拋出一個Error對象。Error對象也可以被看做一個Error原型,用戶可以擴展其含義,以創建自己的 Error 對象。
Error.prototype對象通常包含下面屬性:
constructor - 一個錯誤實例原型的構造函數
message - 錯誤信息
name - 錯誤名稱
這幾個都是標準屬性,有時不同編譯的環境會有其獨特的屬性。在一些環境中,例如 Node 和 Firefox,甚至還有stack屬性,這里面包含了錯誤的 Stack trace。一個Error的堆棧追蹤包含了從其構造函數開始的所有堆棧幀。
如果你想要學習一個Error對象的特殊屬性,我強烈建議你看一下在MDN上的這篇文章。
要拋出一個Error,你必須使用throw關鍵字。為了catch一個拋出的Error,你必須把可能拋出Error的代碼用try塊包起來。然后緊跟著一個catch塊,catch塊中通常會接受一個包含了錯誤信息的參數。
和在 Java 中類似,不論在try中是否拋出Error, JavaScript 中都允許你在try/catch塊后面緊跟著一個finally塊。不論你在try中的操作是否生效,在你操作完以后,都用finally來清理對象,這是個編程的好習慣。
介紹到現在的知識,可能對于大部分人來說,都是已經掌握了的,那么現在我們就進行更深入一些的吧。
使用try塊時,后面可以不跟著catch塊,但是必須跟著finally塊。所以我們就有三種不同形式的try語句:
try...catch
try...finally
try...catch...finally
Try語句也可以內嵌在一個try語句中,如:
try { try { // 這里拋出的Error,將被下面的catch獲取到 throw new Error("Nested error."); } catch (nestedErr) { // 這里會打印出來 console.log("Nested catch"); } } catch (err) { console.log("This will not run."); }
你也可以把try語句內嵌在catch和finally塊中:
try { throw new Error("First error"); } catch (err) { console.log("First catch running"); try { throw new Error("Second error"); } catch (nestedErr) { console.log("Second catch running."); } }
*
try {
console.log("The try block is running...");
} finally {
try { throw new Error("Error inside finally."); } catch (err) { console.log("Caught an error inside the finally block."); }
}
這里給出另外一個重要的提示:你可以拋出非Error對象的值。盡管這看起來很炫酷,很靈活,但實際上這個用法并不好,尤其在一個開發者改另一個開發者寫的庫的時候。因為這樣代碼沒有一個標準,你不知道其他人會拋出什么信息。這樣的話,你就不能簡單的相信拋出的Error信息了,因為有可能它并不是Error信息,而是一個字符串或者一個數字。另外這也導致了如果你需要處理 Stack trace 或者其他有意義的元數據,也將變的很困難。
例如給你下面這段代碼:
function runWithoutThrowing(func) { try { func(); } catch (e) { console.log("There was an error, but I will not throw it."); console.log("The error"s message was: " + e.message) } } function funcThatThrowsError() { throw new TypeError("I am a TypeError."); } runWithoutThrowing(funcThatThrowsError);
這段代碼,如果其他人傳遞一個帶有拋出Error對象的函數給runWithoutThrowing函數的話,將完美運行。然而,如果他拋出一個String類型的話,則情況就麻煩了。
function runWithoutThrowing(func) { try { func(); } catch (e) { console.log("There was an error, but I will not throw it."); console.log("The error"s message was: " + e.message) } } function funcThatThrowsString() { throw "I am a String."; } runWithoutThrowing(funcThatThrowsString);
可以看到這段代碼中,第二個console.log會告訴你這個 Error 信息是undefined。這現在看起來不是很重要,但是如果你需要確定是否這個Error中確實包含某個屬性,或者用另一種方式處理Error的特殊屬性,那你就需要多花很多的功夫了。
另外,當拋出一個非Error對象的值時,你沒有訪問Error對象的一些重要的數據,比如它的堆棧,而這在一些編譯環境中是一個非常重要的Error對象屬性。
Error 還可以當做其他普通對象一樣使用,你并不需要拋出它。這就是為什么它通常作為回調函數的第一個參數,就像fs.readdir函數這樣:
const fs = require("fs"); fs.readdir("/example/i-do-not-exist", function callback(err, dirs) { if (err instanceof Error) { // "readdir"將會拋出一個異常,因為目錄不存在 // 我們可以在我們的回調函數中使用 Error 對象 console.log("Error Message: " + err.message); console.log("See? We can use Errors without using try statements."); } else { console.log(dirs); } });
最后,你也可以在 promise 被 reject 的時候使用Error對象,這使得處理 promise reject 變得很簡單。
new Promise(function(resolve, reject) { reject(new Error("The promise was rejected.")); }).then(function() { console.log("I am an error."); }).catch(function(err) { if (err instanceof Error) { console.log("The promise was rejected with an error."); console.log("Error Message: " + err.message); } });使用 Stack Trace
ok,那么現在,你們所期待的部分來了:如何使用堆棧追蹤。
這一章專門討論支持 Error.captureStackTrace 的環境,如:NodeJS。
Error.captureStackTrace函數的第一個參數是一個object對象,第二個參數是一個可選的function。捕獲堆棧跟蹤所做的是要捕獲當前堆棧的路徑(這是顯而易見的),并且在 object 對象上創建一個stack屬性來存儲它。如果提供了第二個 function 參數,那么這個被傳遞的函數將會被看成是本次堆棧調用的終點,本次堆棧跟蹤只會展示到這個函數被調用之前。
我們來用幾個例子來更清晰的解釋下。我們將捕獲當前堆棧路徑并且將其存儲到一個普通 object 對象中。
const myObj = {}; function c() { } function b() { // 這里存儲當前的堆棧路徑,保存到myObj中 Error.captureStackTrace(myObj); c(); } function a() { b(); } // 首先調用這些函數 a(); // 這里,我們看一下堆棧路徑往 myObj.stack 中存儲了什么 console.log(myObj.stack); // 這里將會打印如下堆棧信息到控制臺 // at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack // at a (repl:2:1) // at repl:1:1 <-- Node internals below this line // at realRunInThisContextScript (vm.js:22:35) // at sigintHandlersWrap (vm.js:98:12) // at ContextifyScript.Script.runInThisContext (vm.js:24:12) // at REPLServer.defaultEval (repl.js:313:29) // at bound (domain.js:280:14) // at REPLServer.runBound [as eval] (domain.js:293:12) // at REPLServer.onLine (repl.js:513:10)
我們從上面的例子中可以看到,我們首先調用了a(a被壓入棧),然后從a的內部調用了b(b被壓入棧,并且在a的上面)。在b中,我們捕獲到了當前堆棧路徑并且將其存儲在了myObj中。這就是為什么打印在控制臺上的只有a和b,而且是下面a上面b。
好的,那么現在,我們傳遞第二個參數到Error.captureStackTrace看看會發生什么?
const myObj = {}; function d() { // 這里存儲當前的堆棧路徑,保存到myObj中 // 這次我們隱藏包含b在內的b以后的所有堆棧幀 Error.captureStackTrace(myObj, b); } function c() { d(); } function b() { c(); } function a() { b(); } // 首先調用這些函數 a(); // 這里,我們看一下堆棧路徑往 myObj.stack 中存儲了什么 console.log(myObj.stack); // 這里將會打印如下堆棧信息到控制臺 // at a (repl:2:1) <-- As you can see here we only get frames before `b` was called // at repl:1:1 <-- Node internals below this line // at realRunInThisContextScript (vm.js:22:35) // at sigintHandlersWrap (vm.js:98:12) // at ContextifyScript.Script.runInThisContext (vm.js:24:12) // at REPLServer.defaultEval (repl.js:313:29) // at bound (domain.js:280:14) // at REPLServer.runBound [as eval] (domain.js:293:12) // at REPLServer.onLine (repl.js:513:10) // at emitOne (events.js:101:20)
當我們傳遞b到Error.captureStackTraceFunction里時,它隱藏了b和在它以上的所有堆棧幀。這就是為什么堆棧路徑里只有a的原因。
看到這,你可能會問這樣一個問題:“為什么這是有用的呢?”。它之所以有用,是因為你可以隱藏所有的內部實現細節,而這些細節其他開發者調用的時候并不需要知道。例如,在 Chai 中,我們用這種方法對我們代碼的調用者屏蔽了不相關的實現細節。
真實場景中的 Stack Trace 處理正如我在上一節中提到的,Chai 用棧處理技術使得堆棧路徑和調用者更加相關,這里是我們如何實現它的。
首先,讓我們來看一下當一個 Assertion 失敗的時候,AssertionError的構造函數做了什么。
// "ssfi"代表"起始堆棧函數",它是移除其他不相關堆棧幀的起始標記 function AssertionError (message, _props, ssf) { var extend = exclude("name", "message", "stack", "constructor", "toJSON") , props = extend(_props || {}); // 默認值 this.message = message || "Unspecified AssertionError"; this.showDiff = false; // 從屬性中copy for (var key in props) { this[key] = props[key]; } // 這里是和我們相關的 // 如果提供了起始堆棧函數,那么我們從當前堆棧路徑中獲取到, // 并且將其傳遞給"captureStackTrace",以保證移除其后的所有幀 ssf = ssf || arguments.callee; if (ssf && Error.captureStackTrace) { Error.captureStackTrace(this, ssf); } else { // 如果沒有提供起始堆棧函數,那么使用原始堆棧 try { throw new Error(); } catch(e) { this.stack = e.stack; } } }
正如你在上面可以看到的,我們使用了Error.captureStackTrace來捕獲堆棧路徑,并且把它存儲在我們所創建的一個AssertionError實例中。然后傳遞了一個起始堆棧函數進去(用if判斷如果存在則傳遞),這樣就從堆棧路徑中移除掉了不相關的堆棧幀,不顯示一些內部實現細節,保證了堆棧信息的“清潔”。
感興趣的讀者可以繼續看一下最近 @meeber 在 這里 的代碼。
在我們繼續看下面的代碼之前,我要先告訴你addChainableMethod都做了什么。它添加所傳遞的可以被鏈式調用的方法到 Assertion,并且用包含了 Assertion 的方法標記 Assertion 本身。用ssfi(表示起始堆棧函數指示器)這個名字記錄。這意味著當前 Assertion 就是堆棧的最后一幀,就是說不會再多顯示任何 Chai 項目中的內部實現細節了。我在這里就不多列出來其整個代碼了,里面用了很多 trick 的方法,但是如果你想了解更多,可以從 這個鏈接 里獲取到。
在下面的代碼中,展示了lengthOf的 Assertion 的邏輯,它是用來檢查一個對象的確定長度的。我們希望調用我們函數的開發者這樣來使用:expect(["foo", "bar"]).to.have.lengthOf(2)。
function assertLength (n, msg) { if (msg) flag(this, "message", msg); var obj = flag(this, "object") , ssfi = flag(this, "ssfi"); // 密切關注這一行 new Assertion(obj, msg, ssfi, true).to.have.property("length"); var len = obj.length; // 這一行也是相關的 this.assert( len == n , "expected #{this} to have a length of #{exp} but got #{act}" , "expected #{this} to not have a length of #{act}" , n , len ); } Assertion.addChainableMethod("lengthOf", assertLength, assertLengthChain);
在代碼中,我著重對跟我們相關的代碼進行了注釋,我們從this.assert的調用開始。
下面是this.assert方法的代碼:
Assertion.prototype.assert = function (expr, msg, negateMsg, expected, _actual, showDiff) { var ok = util.test(this, arguments); if (false !== showDiff) showDiff = true; if (undefined === expected && undefined === _actual) showDiff = false; if (true !== config.showDiff) showDiff = false; if (!ok) { msg = util.getMessage(this, arguments); var actual = util.getActual(this, arguments); // 這是和我們相關的行 throw new AssertionError(msg, { actual: actual , expected: expected , showDiff: showDiff }, (config.includeStack) ? this.assert : flag(this, "ssfi")); } };
assert方法主要用來檢查 Assertion 的布爾表達式是真還是假。如果是假,則我們必須實例化一個AssertionError。這里注意,當我們實例化一個AssertionError對象的時候,我們也傳遞了一個起始堆棧函數指示器(ssfi)。如果配置標記includeStack是打開的,我們通過傳遞一個this.assert給調用者,以向他展示整個堆棧路徑。可是,如果includeStack配置是關閉的,我們則必須從堆棧路徑中隱藏內部實現細節,這就需要用到存儲在ssfi中的標記了。
ok,那么我們再來討論一下其他和我們相關的代碼:
new Assertion(obj, msg, ssfi, true).to.have.property("length");
可以看到,當創建這個內嵌 Assertion 的時候,我們傳遞了ssfi中已獲取到的內容。這意味著,當創建一個新的 Assertion 時,將使用這個函數來作為從堆棧路徑中移除無用堆棧幀的起始點。順便說一下,下面這段代碼是Assertion的構造函數。
function Assertion (obj, msg, ssfi, lockSsfi) { // 這是和我們相關的行 flag(this, "ssfi", ssfi || Assertion); flag(this, "lockSsfi", lockSsfi); flag(this, "object", obj); flag(this, "message", msg); return util.proxify(this); }
還記得我在講述addChainableMethod時說的,它用包含他自己的方法設置的ssfi標記,這就意味著這是堆棧路徑中最底層的內部幀,我們可以移除在它之上的所有幀。
回想上面的代碼,內嵌 Assertion 用來判斷對象是不是有合適的長度(Length)。傳遞ssfi到這個 Assertion 中,要避免重置我們要將其作為起始指示器的堆棧幀,并且使先前的addChainableMethod在堆棧中保持可見狀態。
這看起來可能有點復雜,現在我們重新回顧一下,我們想要移除沒有用的堆棧幀都做了什么工作:
當我們運行一個 Assertion 時,我們設置它本身來作為我們移除其后面堆棧幀的標記。
這個 Assertion 開始執行,如果判斷失敗,那么從剛才我們所存儲的那個標記開始,移除其后面所有的內部幀。
如果有內嵌 Assertion,那么我們必須要使用包含當前 Assertion 的方法作為移除后面堆棧幀的標記,即放到ssfi中。因此我們要傳遞當前ssfi(起始堆棧函數指示器)到我們即將要新創建的內嵌 Assertion 中來存儲起來。
最后我還是強烈建議來閱讀一下 @meeber的評論 來加深對它的理解。
我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/81597.html
摘要:封裝手寫的方筆記使用檢測文件前端掘金副標題可以做什么以及使用中會遇到的坑。目的是幫助人們用純中文指南實現復選框中多選功能前端掘金作者緝熙簡介是推出的一個天挑戰。 深入理解 JavaScript Errors 和 Stack Traces - 前端 - 掘金譯者注:本文作者是著名 JavaScript BDD 測試框架 Chai.js 源碼貢獻者之一,Chai.js 中會遇到很多異常處理...
摘要:封裝手寫的方筆記使用檢測文件前端掘金副標題可以做什么以及使用中會遇到的坑。目的是幫助人們用純中文指南實現復選框中多選功能前端掘金作者緝熙簡介是推出的一個天挑戰。 深入理解 JavaScript Errors 和 Stack Traces - 前端 - 掘金譯者注:本文作者是著名 JavaScript BDD 測試框架 Chai.js 源碼貢獻者之一,Chai.js 中會遇到很多異常處理...
摘要:調用堆棧實際上就是一個方法列表,按調用順序保存所有在運行期被調用的方法。調用堆棧會將當前正在執行的函數調用壓入堆棧,一旦函數調用結束,又會將它移出堆棧。 原文 JavaScript Errors and Stack Traces in Depth 調用棧Call Stack是如何工作的 棧是一個后進先出LIFO (Last in,First out)的數據結構。調用堆棧實際上就是一個方...
摘要:譯者注翻譯一個對新手比較友好的工作原理解析系列文章注意以下全部是概念經驗豐富的老鳥可以離場啦正文從這里開始隨著的流行團隊們正在利用來支持多個級別的技術棧包括前端后端混合開發嵌入式設備以及更多這篇文章旨在成為深入挖掘和實際上他是怎么工作的系列 譯者注 翻譯一個對新手比較友好的 JavaScript 工作原理解析系列文章 注意: 以下全部是概念,經驗豐富的老鳥可以離場啦 正文從這里開始 隨...
摘要:監聽上報應用無響應是數據采集系統功能之一,本文講述一種可行實現方案。是一個用于監聽文件訪問創建修改刪除移動等操作的監聽器。為此本文同時提供一種線程輪詢措施,用于輔助監聽。 監聽上報ANR(Application Not Responding,應用無響應)是數據采集系統功能之一,本文講述一種可行實現方案。 方案概述 ANR一般有三種類型[1]: KeyDispatchTimeout(5...
閱讀 1599·2021-11-22 09:34
閱讀 1690·2019-08-29 16:36
閱讀 2668·2019-08-29 15:43
閱讀 3113·2019-08-29 13:57
閱讀 1297·2019-08-28 18:05
閱讀 1874·2019-08-26 18:26
閱讀 3242·2019-08-26 10:39
閱讀 3454·2019-08-23 18:40