成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深入理解JavaScript Errors和Stack Traces

開發 開發工具
本文我們聊聊Errors和Stack traces以及如何熟練地使用它們。

[[186908]]

這次我們聊聊 Errors 和 Stack traces 以及如何熟練地使用它們。

很多同學并不重視這些細節,但是這些知識在你寫 Testing 和 Error 相關的 lib 的時候是非常有用的。使用 Stack traces 可以清理無用的數據,讓你關注真正重要的問題。同時,你真正理解 Errors 和它們的屬性到底是什么的時候,你將會更有信心的使用它們。

這篇文章在開始的時候看起來比較簡單,但當你熟練運用 Stack trace 以后則會感到非常復雜。所以在看難的章節之前,請確保你理解了前面的內容。

一、Stack是如何工作的

在我們談到 Errors 之前,我們必須理解 Stack 是如何工作的。它其實非常簡單,但是在開始之前了解它也是非常必要的。如果你已經知道了這些,可以略過這一章節。

每當有一個函數調用,就會將其壓入棧頂。在調用結束的時候再將其從棧頂移出。

這種有趣的數據結構叫做“***一個進入的,將會***個出去”。這就是廣為所知的 LIFO(后進先出)。

舉個例子,在函數 x 的內部調用了函數 y,這時棧中就有個順序先 x 后 y。我再舉另外一個例子,看下面代碼:

  1. function c() { 
  2.     console.log('c'); 
  3.  
  4. function b() { 
  5.     console.log('b'); 
  6.     c(); 
  7.  
  8. function a() { 
  9.     console.log('a'); 
  10.     b(); 
  11.  
  12. a(); 

上面的這段代碼,當運行 a 的時候,它會被壓到棧頂。然后,當 b 在 a 中被調用的時候,它會被繼續壓入棧頂,當 c 在 b 中被調用的時候,也一樣。

在運行 c 的時候,棧中包含了 a,b,c,并且其順序也是 a,b,c。

當 c 調用完畢時,它會被從棧頂移出,隨后控制流回到 b。當 b 執行完畢后也會從棧頂移出,控制流交還到 a。***,當 a 執行完畢后也會從棧中移出。

為了更好的展示這樣一種行為,我們用 console.trace() 來將 Stack trace 打印到控制臺上來。通常我們讀 Stack traces 信息的時候是從上往下讀的。

  1. function c() { 
  2.     console.log('c'); 
  3.     console.trace(); 
  4.  
  5. function b() { 
  6.     console.log('b'); 
  7.     c(); 
  8.  
  9. function a() { 
  10.     console.log('a'); 
  11.     b(); 
  12.  
  13. a(); 

當我們在 Node REPL 服務端執行的時候,會返回如下:

  1. Trace 
  2.     at c (repl:3:9) 
  3.     at b (repl:3:1) 
  4.     at a (repl:3:1) 
  5.     at repl:1:1 // <-- For now feel free to ignore anything below this point, these are Node's internals 
  6.     at realRunInThisContextScript (vm.js:22:35) 
  7.     at sigintHandlersWrap (vm.js:98:12) 
  8.     at ContextifyScript.Script.runInThisContext (vm.js:24:12) 
  9.     at REPLServer.defaultEval (repl.js:313:29) 
  10.     at bound (domain.js:280:14) 
  11.     at REPLServer.runBound [as eval] (domain.js:293:12) 

從上面我們可以看到,當棧信息從 c 中打印出來的時候,我看到了 a,b 和 c?,F在,如果在 c 執行完畢以后,在 b 中把 Stack trace 打印出來,我們可以看到 c 已經從棧中移出了,棧中只有 a 和 b。

  1. function c() { 
  2.     console.log('c'); 
  3.  
  4. function b() { 
  5.     console.log('b'); 
  6.     c(); 
  7.     console.trace(); 
  8.  
  9. function a() { 
  10.     console.log('a'); 
  11.     b(); 
  12.  
  13. a(); 

下面可以看到,c 已經不在棧中了,在其執行完以后,從棧中 pop 出去了。

  1. Trace 
  2.     at b (repl:4:9) 
  3.     at a (repl:3:1) 
  4.     at repl:1:1  // <-- For now feel free to ignore anything below this point, these are Node's internals 
  5.     at realRunInThisContextScript (vm.js:22:35) 
  6.     at sigintHandlersWrap (vm.js:98:12) 
  7.     at ContextifyScript.Script.runInThisContext (vm.js:24:12) 
  8.     at REPLServer.defaultEval (repl.js:313:29) 
  9.     at bound (domain.js:280:14) 
  10.     at REPLServer.runBound [as eval] (domain.js:293:12) 
  11.     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 語句中,如:

  1. try { 
  2.     try { 
  3.         // 這里拋出的Error,將被下面的catch獲取到 
  4.         throw new Error('Nested error.');  
  5.     } catch (nestedErr) { 
  6.         // 這里會打印出來 
  7.         console.log('Nested catch'); 
  8.     } 
  9. } catch (err) { 
  10.     console.log('This will not run.'); 

你也可以把 try 語句內嵌在 catch 和 finally 塊中:

  1. try { 
  2.     throw new Error('First error'); 
  3. } catch (err) { 
  4.     console.log('First catch running'); 
  5.     try { 
  6.         throw new Error('Second error'); 
  7.     } catch (nestedErr) { 
  8.         console.log('Second catch running.'); 
  9.     } 
  1. try { 
  2.     console.log('The try block is running...'); 
  3. } finally { 
  4.     try { 
  5.         throw new Error('Error inside finally.'); 
  6.     } catch (err) { 
  7.         console.log('Caught an error inside the finally block.'); 
  8.     } 

這里給出另外一個重要的提示:你可以拋出非 Error 對象的值。盡管這看起來很炫酷,很靈活,但實際上這個用法并不好,尤其在一個開發者改另一個開發者寫的庫的時候。因為這樣代碼沒有一個標準,你不知道其他人會拋出什么信息。這樣的話,你就不能簡單的相信拋出的 Error 信息了,因為有可能它并不是 Error 信息,而是一個字符串或者一個數字。另外這也導致了如果你需要處理 Stack trace 或者其他有意義的元數據,也將變的很困難。

例如給你下面這段代碼:

  1. function runWithoutThrowing(func) { 
  2.     try { 
  3.         func(); 
  4.     } catch (e) { 
  5.         console.log('There was an error, but I will not throw it.'); 
  6.         console.log('The error\'s message was: ' + e.message) 
  7.     } 
  8.  
  9. function funcThatThrowsError() { 
  10.     throw new TypeError('I am a TypeError.'); 
  11.  
  12. runWithoutThrowing(funcThatThrowsError); 

這段代碼,如果其他人傳遞一個帶有拋出 Error 對象的函數給 runWithoutThrowing 函數的話,將***運行。然而,如果他拋出一個 String 類型的話,則情況就麻煩了。

  1. function runWithoutThrowing(func) { 
  2.     try { 
  3.         func(); 
  4.     } catch (e) { 
  5.         console.log('There was an error, but I will not throw it.'); 
  6.         console.log('The error\'s message was: ' + e.message) 
  7.     } 
  8.  
  9. function funcThatThrowsString() { 
  10.     throw 'I am a String.'; 
  11.  
  12. runWithoutThrowing(funcThatThrowsString); 

可以看到這段代碼中,第二個 console.log 會告訴你這個 Error 信息是 undefined。這現在看起來不是很重要,但是如果你需要確定是否這個 Error 中確實包含某個屬性,或者用另一種方式處理 Error 的特殊屬性,那你就需要多花很多的功夫了。

另外,當拋出一個非 Error 對象的值時,你沒有訪問 Error 對象的一些重要的數據,比如它的堆棧,而這在一些編譯環境中是一個非常重要的 Error 對象屬性。

Error 還可以當做其他普通對象一樣使用,你并不需要拋出它。這就是為什么它通常作為回調函數的***個參數,就像 fs.readdir 函數這樣:

  1. const fs = require('fs'); 
  2.  
  3. fs.readdir('/example/i-do-not-exist', function callback(err, dirs) { 
  4.     if (err instanceof Error) { 
  5.         // 'readdir'將會拋出一個異常,因為目錄不存在 
  6.         // 我們可以在我們的回調函數中使用 Error 對象 
  7.         console.log('Error Message: ' + err.message); 
  8.         console.log('See? We can use  Errors  without using try statements.'); 
  9.     } else { 
  10.         console.log(dirs); 
  11.     } 
  12. }); 

***,你也可以在 promise 被 reject 的時候使用 Error 對象,這使得處理 promise reject 變得很簡單。

  1. new Promise(function(resolve, reject) { 
  2.     reject(new Error('The promise was rejected.')); 
  3. }).then(function() { 
  4.     console.log('I am an error.'); 
  5. }).catch(function(err) { 
  6.     if (err instanceof Error) { 
  7.         console.log('The promise was rejected with an error.'); 
  8.         console.log('Error Message: ' + err.message); 
  9.     } 
  10. }); 

三、使用 Stack Trace

ok,那么現在,你們所期待的部分來了:如何使用堆棧追蹤。

這一章專門討論支持 Error.captureStackTrace 的環境,如:NodeJS。

Error.captureStackTrace 函數的***個參數是一個 object 對象,第二個參數是一個可選的 function。捕獲堆棧跟蹤所做的是要捕獲當前堆棧的路徑(這是顯而易見的),并且在 object 對象上創建一個 stack 屬性來存儲它。如果提供了第二個 function 參數,那么這個被傳遞的函數將會被看成是本次堆棧調用的終點,本次堆棧跟蹤只會展示到這個函數被調用之前。

我們來用幾個例子來更清晰的解釋下。我們將捕獲當前堆棧路徑并且將其存儲到一個普通 object 對象中。

  1. const myObj = {}; 
  2.  
  3. function c() { 
  4.  
  5. function b() { 
  6.     // 這里存儲當前的堆棧路徑,保存到myObj中 
  7.     Error.captureStackTrace(myObj); 
  8.     c(); 
  9.  
  10. function a() { 
  11.     b(); 
  12.  
  13. // 首先調用這些函數 
  14. a(); 
  15.  
  16. // 這里,我們看一下堆棧路徑往 myObj.stack 中存儲了什么 
  17. console.log(myObj.stack); 
  18.  
  19. // 這里將會打印如下堆棧信息到控制臺 
  20. //    at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack 
  21. //    at a (repl:2:1) 
  22. //    at repl:1:1 <-- Node internals below this line 
  23. //    at realRunInThisContextScript (vm.js:22:35) 
  24. //    at sigintHandlersWrap (vm.js:98:12) 
  25. //    at ContextifyScript.Script.runInThisContext (vm.js:24:12) 
  26. //    at REPLServer.defaultEval (repl.js:313:29) 
  27. //    at bound (domain.js:280:14) 
  28. //    at REPLServer.runBound [as eval] (domain.js:293:12) 
  29. //    at REPLServer.onLine (repl.js:513:10) 

我們從上面的例子中可以看到,我們首先調用了a(a被壓入棧),然后從a的內部調用了b(b被壓入棧,并且在a的上面)。在b中,我們捕獲到了當前堆棧路徑并且將其存儲在了 myObj 中。這就是為什么打印在控制臺上的只有a和b,而且是下面a上面b。

好的,那么現在,我們傳遞第二個參數到 Error.captureStackTrace 看看會發生什么?

  1. const myObj = {}; 
  2.  
  3. function d() { 
  4.     // 這里存儲當前的堆棧路徑,保存到myObj中 
  5.     // 這次我們隱藏包含b在內的b以后的所有堆棧幀 
  6.     Error.captureStackTrace(myObj, b); 
  7.  
  8. function c() { 
  9.     d(); 
  10.  
  11. function b() { 
  12.     c(); 
  13.  
  14. function a() { 
  15.     b(); 
  16.  
  17. // 首先調用這些函數 
  18. a(); 
  19.  
  20. // 這里,我們看一下堆棧路徑往 myObj.stack 中存儲了什么 
  21. console.log(myObj.stack); 
  22.  
  23. // 這里將會打印如下堆棧信息到控制臺 
  24. //    at a (repl:2:1) <-- As you can see here we only get frames before `b` was called 
  25. //    at repl:1:1 <-- Node internals below this line 
  26. //    at realRunInThisContextScript (vm.js:22:35) 
  27. //    at sigintHandlersWrap (vm.js:98:12) 
  28. //    at ContextifyScript.Script.runInThisContext (vm.js:24:12) 
  29. //    at REPLServer.defaultEval (repl.js:313:29) 
  30. //    at bound (domain.js:280:14) 
  31. //    at REPLServer.runBound [as eval] (domain.js:293:12) 
  32. //    at REPLServer.onLine (repl.js:513:10) 
  33. //    at emitOne (events.js:101:20) 

當我們傳遞 b 到 Error.captureStackTraceFunction 里時,它隱藏了 b 和在它以上的所有堆棧幀。這就是為什么堆棧路徑里只有a的原因。

看到這,你可能會問這樣一個問題:“為什么這是有用的呢?”。它之所以有用,是因為你可以隱藏所有的內部實現細節,而這些細節其他開發者調用的時候并不需要知道。例如,在 Chai 中,我們用這種方法對我們代碼的調用者屏蔽了不相關的實現細節。

四、真實場景中的 Stack Trace 處理

正如我在上一節中提到的,Chai 用棧處理技術使得堆棧路徑和調用者更加相關,這里是我們如何實現它的。

首先,讓我們來看一下當一個 Assertion 失敗的時候,AssertionError 的構造函數做了什么。

  1. // 'ssfi'代表"起始堆棧函數",它是移除其他不相關堆棧幀的起始標記 
  2. function AssertionError (message, _props, ssf) { 
  3.   var extend = exclude('name', 'message', 'stack', 'constructor', 'toJSON') 
  4.     , props = extend(_props || {}); 
  5.  
  6.   // 默認值 
  7.   this.message = message || 'Unspecified AssertionError'; 
  8.   this.showDiff = false
  9.  
  10.   // 從屬性中copy 
  11.   for (var key in props) { 
  12.     this[key] = props[key]; 
  13.   } 
  14.  
  15.   // 這里是和我們相關的 
  16.   // 如果提供了起始堆棧函數,那么我們從當前堆棧路徑中獲取到, 
  17.   // 并且將其傳遞給'captureStackTrace',以保證移除其后的所有幀 
  18.   ssfssf = ssf || arguments.callee; 
  19.   if (ssf && Error.captureStackTrace) { 
  20.     Error.captureStackTrace(this, ssf); 
  21.   } else { 
  22.     // 如果沒有提供起始堆棧函數,那么使用原始堆棧 
  23.     try { 
  24.       throw new Error(); 
  25.     } catch(e) { 
  26.       this.stack = e.stack; 
  27.     } 
  28.   } 

正如你在上面可以看到的,我們使用了 Error.captureStackTrace 來捕獲堆棧路徑,并且把它存儲在我們所創建的一個 AssertionError 實例中。然后傳遞了一個起始堆棧函數進去(用if判斷如果存在則傳遞),這樣就從堆棧路徑中移除掉了不相關的堆棧幀,不顯示一些內部實現細節,保證了堆棧信息的“清潔”。

在我們繼續看下面的代碼之前,我要先告訴你 addChainableMethod 都做了什么。它添加所傳遞的可以被鏈式調用的方法到 Assertion,并且用包含了 Assertion 的方法標記 Assertion 本身。用ssfi(表示起始堆棧函數指示器)這個名字記錄。這意味著當前 Assertion 就是堆棧的***一幀,就是說不會再多顯示任何 Chai 項目中的內部實現細節了。我在這里就不多列出來其整個代碼了,里面用了很多 trick 的方法,但是如果你想了解更多,可以從 這個鏈接 里獲取到。

在下面的代碼中,展示了 lengthOf 的 Assertion 的邏輯,它是用來檢查一個對象的確定長度的。我們希望調用我們函數的開發者這樣來使用:expect(['foo', 'bar']).to.have.lengthOf(2)。

  1. function assertLength (n, msg) { 
  2.     if (msg) flag(this, 'message', msg); 
  3.     var obj = flag(this, 'object') 
  4.         , ssfi = flag(this, 'ssfi'); 
  5.  
  6.     // 密切關注這一行 
  7.     new Assertion(obj, msg, ssfi, true).to.have.property('length'); 
  8.     var len = obj.length; 
  9.  
  10.     // 這一行也是相關的 
  11.     this.assert( 
  12.             len == n 
  13.         , 'expected #{this} to have a length of #{exp} but got #{act}' 
  14.         , 'expected #{this} to not have a length of #{act}' 
  15.         , n 
  16.         , len 
  17.     ); 
  18.  
  19. Assertion.addChainableMethod('lengthOf', assertLength, assertLengthChain); 

下面是 this.assert 方法的代碼:

  1. function assertLength (n, msg) { 
  2.     if (msg) flag(this, 'message', msg); 
  3.     var obj = flag(this, 'object') 
  4.         , ssfi = flag(this, 'ssfi'); 
  5.  
  6.     // 密切關注這一行 
  7.     new Assertion(obj, msg, ssfi, true).to.have.property('length'); 
  8.     var len = obj.length; 
  9.  
  10.     // 這一行也是相關的 
  11.     this.assert( 
  12.             len == n 
  13.         , 'expected #{this} to have a length of #{exp} but got #{act}' 
  14.         , 'expected #{this} to not have a length of #{act}' 
  15.         , n 
  16.         , len 
  17.     ); 
  18.  
  19. Assertion.addChainableMethod('lengthOf', assertLength, assertLengthChain); 

assert 方法主要用來檢查 Assertion 的布爾表達式是真還是假。如果是假,則我們必須實例化一個 AssertionError。這里注意,當我們實例化一個 AssertionError 對象的時候,我們也傳遞了一個起始堆棧函數指示器(ssfi)。如果配置標記 includeStack 是打開的,我們通過傳遞一個 this.assert 給調用者,以向他展示整個堆棧路徑??墒?,如果 includeStack 配置是關閉的,我們則必須從堆棧路徑中隱藏內部實現細節,這就需要用到存儲在 ssfi 中的標記了。

ok,那么我們再來討論一下其他和我們相關的代碼:

  1. new Assertion(obj, msg, ssfi, true).to.have.property('length'); 

可以看到,當創建這個內嵌 Assertion 的時候,我們傳遞了 ssfi 中已獲取到的內容。這意味著,當創建一個新的 Assertion 時,將使用這個函數來作為從堆棧路徑中移除無用堆棧幀的起始點。順便說一下,下面這段代碼是 Assertion 的構造函數。

  1. function Assertion (obj, msg, ssfi, lockSsfi) { 
  2.     // 這是和我們相關的行 
  3.     flag(this, 'ssfi', ssfi || Assertion); 
  4.     flag(this, 'lockSsfi', lockSsfi); 
  5.     flag(this, 'object', obj); 
  6.     flag(this, 'message', msg); 
  7.  
  8.     return util.proxify(this); 

還記得我在講述 addChainableMethod 時說的,它用包含他自己的方法設置的 ssfi 標記,這就意味著這是堆棧路徑中***層的內部幀,我們可以移除在它之上的所有幀。

回想上面的代碼,內嵌 Assertion 用來判斷對象是不是有合適的長度(Length)。傳遞 ssfi 到這個 Assertion 中,要避免重置我們要將其作為起始指示器的堆棧幀,并且使先前的 addChainableMethod 在堆棧中保持可見狀態。

這看起來可能有點復雜,現在我們重新回顧一下,我們想要移除沒有用的堆棧幀都做了什么工作:

  1. 當我們運行一個 Assertion 時,我們設置它本身來作為我們移除其后面堆棧幀的標記。
  2. 這個 Assertion 開始執行,如果判斷失敗,那么從剛才我們所存儲的那個標記開始,移除其后面所有的內部幀。
  3. 如果有內嵌 Assertion,那么我們必須要使用包含當前 Assertion 的方法作為移除后面堆棧幀的標記,即放到 ssfi 中。因此我們要傳遞當前 ssfi(起始堆棧函數指示器)到我們即將要新創建的內嵌 Assertion 中來存儲起來。

點擊《深入理解 JavaScript Errors 和 Stack Traces》閱讀原文。

【本文是51CTO專欄作者“胡子大哈”的原創文章,轉載請聯系作者本人獲取授權】

戳這里,看該作者更多好文

責任編輯:趙寧寧 來源: 51CTO專欄
相關推薦

2021-02-17 11:25:33

前端JavaScriptthis

2017-04-25 15:30:23

堆棧函數JavaScript

2015-11-04 09:57:18

JavaScript原型

2019-11-05 10:03:08

callback回調函數javascript

2024-07-18 10:12:04

2013-11-05 13:29:04

JavaScriptreplace

2011-09-06 09:56:24

JavaScript

2019-03-13 08:00:00

JavaScript作用域前端

2020-07-24 10:00:00

JavaScript執行上下文前端

2020-12-16 09:47:01

JavaScript箭頭函數開發

2011-03-02 12:33:00

JavaScript

2012-01-05 15:07:11

JavaScript

2024-05-08 13:52:04

JavaScriptWeb應用程序

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數據結構hash函數

2020-07-21 08:26:08

SpringSecurity過濾器

2012-08-31 10:00:12

Hadoop云計算群集網絡

2012-11-08 14:47:52

Hadoop集群

2013-07-31 10:04:42

hadoopHadoop集群集群和網絡

2024-09-02 14:12:56

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日本精品裸体写真集在线观看 | 九九视频网 | 夜夜操天天干 | 国产福利视频导航 | 日韩av一区二区在线 | 美女在线视频一区二区三区 | 色嗨嗨| 亚洲欧美一区二区三区1000 | 欧美性受xxxx白人性爽 | av在线一区二区三区 | 国产69久久精品成人看动漫 | 久久久一区二区三区 | 国产女人第一次做爰毛片 | 欧美二区在线 | 色资源在线视频 | 亚洲v日韩v综合v精品v | 激情一区二区三区 | 青青草国产在线观看 | 中文字幕成人在线 | 中文精品视频 | 国产成人a亚洲精品 | 国产小u女发育末成年 | 国产亚洲区 | 精品视频一区二区在线观看 | 91中文字幕在线 | 午夜电影福利 | 免费在线视频一区二区 | 国产成人免费一区二区60岁 | 很黄很污的网站 | 日本精品久久久一区二区三区 | 欧美精品一区二区三区蜜臀 | 一级毛片免费完整视频 | 婷婷五月色综合香五月 | 亚洲高清在线视频 | 国产一区二区三区四区三区四 | 91久久久久久久久久久 | 一区二区三区四区视频 | 国产精品亚洲一区二区三区在线观看 | 色综合久 | 日韩影院在线观看 | 男人的天堂久久 |