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

深入理解JavaScript錯誤和堆棧追蹤

開發 前端
有時候人們并不關注這些細節,但這方面的知識肯定有用,尤其是當你正在編寫與測試或errors相關的庫。例如這個星期我們的chai中出現了一個令人驚嘆的Pull Request,它大大改進了我們處理堆棧跟蹤的方式,并在用戶斷言失敗時提供了更多的信息。

有時候人們并不關注這些細節,但這方面的知識肯定有用,尤其是當你正在編寫與測試或errors相關的庫。例如這個星期我們的chai中出現了一個令人驚嘆的Pull Request,它大大改進了我們處理堆棧跟蹤的方式,并在用戶斷言失敗時提供了更多的信息。

深入理解JavaScript錯誤和堆棧追蹤

操作堆棧記錄可以讓你清理無用數據,并集中精力處理重要事項。此外,當你真正弄清楚Error及其屬性,你將會更有信心地利用它。

本文開頭部分或許太過于簡單,但當你開始處理堆棧記錄時,它將變得稍微有些復雜,所以請確保你在開始這個那部分章節之前已經充分理解前面的內容。

堆棧調用如何工作

在談論errors之前我們必須明白堆棧調用如何工作。它非常簡單,但對于我們將要深入的內容而言卻是至關重要的。如果你已經知道這部分內容,請隨時跳過本節。

每當函數被調用,它都會被推到堆棧的頂部。函數執行完畢,便會從堆棧頂部移除。

這種數據結構的有趣之處在于***一個入棧的將會***個從堆棧中移除,這也就是我們所熟悉的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函數時,a便會添加到堆棧的頂部,然后當b函數在a函數中被調用,b也會被添加到堆棧的頂部,依次類推,在b中調用c也會發生同樣的事情。

當c執行時,堆棧中的函數的順序為a b c

c執行完畢后便會從棧頂移除,這時控制流重新回到了b中,b執行完畢同樣也會從棧頂移除,***控制流又回到了a中,***a執行完畢,a也從堆棧中移除。

我們可以利用console.trace()來更好的演示這種行為,它會在控制臺打印出當前堆棧中的記錄。此外,通常而言你應該從上到下讀取堆棧記錄。想想下面的每一行代碼都是在哪調用的。

  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。

如果我們現在在b中并且在c執行完之后打印堆棧,我們將會發現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. a(); 

正如你看到的那樣,堆棧中已經沒有c,因為它已經完成運行,已經被彈出去了。

  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.prototype對象通常有以下屬性:

  • constructor- 實例原型的構造函數。
  • message - 錯誤信息
  • name - 錯誤名稱

以上都是標準屬性,(但)有時候每個環境都有其特定的屬性,在例如Node,Firefox,Chorme,Edge,IE 10+,Opera 和 Safari 6+ 中,還有一個包含錯誤堆棧記錄的stack屬性。錯誤堆棧記錄包含從(堆棧底部)它自己的構造函數到(堆棧頂部)所有的堆棧幀。

如果想了解更多關于Error對象的具體屬性,我強烈推薦MDN上的這篇文章。

拋出錯誤必須使用throw關鍵字,你必須將可能拋出錯誤的代碼包裹在try代碼塊內并緊跟著一個catch代碼塊來捕獲拋出的錯誤。

正如Java中的錯誤處理,try/catch代碼塊后緊跟著一個finally代碼塊在JavaScript中也是同樣允許的,無論try代碼塊內是否拋出異常,finally代碼塊內的代碼都會執行。在完成處理之后,***實踐是在finally代碼塊中做一些清理的事情,(因為)無論你的操作是否生效,都不會影響到它的執行。

(鑒于)上面所談到的所有事情對大多數人來講都是小菜一碟,那么就讓我們來談一些不為人所知的細節。

try代碼塊后面不必緊跟著catch,但(此種情況下)其后必須緊跟著finally。這意味著我們可以使用三種不同形式的try語句:

  • try...catch
  • try...finally
  • try...catch...finally

Try語句可以像下面這樣互相嵌套:

  1. try { 
  2.     try { 
  3.         throw new Error('Nested error.'); // The error thrown here will be caught by its own `catch` clause 
  4.     } catch (nestedErr) { 
  5.         console.log('Nested catch'); // This runs 
  6.     } 
  7. } catch (err) { 
  8.     console.log('This will not run.'); 

你甚至還可以在catch和finally代碼塊中嵌套try語句:

  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.     } 
  10.  
  11. try { 
  12.     console.log('The try block is running...'); 
  13. } finally { 
  14.     try { 
  15.         throw new Error('Error inside finally.'); 
  16.     } catch (err) { 
  17.         console.log('Caught an error inside the finally block.'); 
  18.     } 

還有很重要的一點值得注意,那就是我們甚至可以大可不必拋出Error對象。盡管這看起來非常cool且非常自由,但實際并非如此,尤其是對開發第三方庫的開發者來說,因為他們必須處理用戶(使用庫的開發者)的代碼。由于缺乏標準,他們并不能把控用戶的行為。你不能相信用戶并簡單的拋出一個Error對象,因為他們不一定會那么做而是僅僅拋出一個字符串或者數字(鬼知道用戶會拋出什么)。這也使得處理必要的堆棧跟蹤和其他有意義的元數據變得更加困難。

假設有以下代碼:

  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會打印出 the error’s message is undefined.這么看來也沒多大的事(后果)呀,但是如果您需要確保某些屬性存在于Error對象上,或以另一種方式(例如Chai的throws斷言 does))處理Error對象的特定屬性,那么你做需要更多的工作,以確保它會正常工資。

此外,當拋出的值不是Error對象時,你無法訪問其他重要數據,例如stack,在某些環境中它是Error對象的一個屬性。

Errors也可以像其他任何對象一樣使用,并不一定非得要拋出他們,這也是它們為什么多次被用作回調函數的***個參數(俗稱 err first)。 在下面的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` will throw an error because that directory does not exist 
  6.         // We will now be able to use the error object passed by it in our callback function 
  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. }); 

***,在rejecting promises時也可以使用Error對象。這使得它更容易處理promise rejections:

  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. }); 

操縱堆棧跟蹤

上面啰嗦了那么多,***的重頭戲來了,那就是如何操縱堆棧跟蹤。

本章專門針對那些像NodeJS支Error.captureStackTrace的環境。

Error.captureStackTrace函數接受一個object作為***個參數,第二個參數是可選的,接受一個函數。capture stack trace 捕獲當前堆棧跟蹤,并在目標對象中創建一個stack屬性來存儲它。如果提供了第二個參數,則傳遞的函數將被視為調用堆棧的終點,因此堆棧跟蹤將僅顯示調用該函數之前發生的調用。

讓我們用例子來說明這一點。首先,我們將捕獲當前堆棧跟蹤并將其存儲在公共對象中。

  1. const myObj = {}; 
  2.  
  3. function c() { 
  4.  
  5. function b() { 
  6.     // Here we will store the current stack trace into myObj 
  7.     Error.captureStackTrace(myObj); 
  8.     c(); 
  9.  
  10. function a() { 
  11.     b(); 
  12.  
  13. // First we will call these functions 
  14. a(); 
  15.  
  16. // Now let's see what is the stack trace stored into myObj.stack 
  17. console.log(myObj.stack); 
  18.  
  19. // This will print the following stack to the console: 
  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中。因此在控制臺中才會按照b a的順序打印堆棧。

現在讓我們給Error.captureStackTrace傳遞一個函數作為第二個參數,看看會發生什么:

  1. const myObj = {}; 
  2.  
  3. function d() { 
  4.     // Here we will store the current stack trace into myObj 
  5.     // This time we will hide all the frames after `b` and `b` itself 
  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. // First we will call these functions 
  18. a(); 
  19.  
  20. // Now let's see what is the stack trace stored into myObj.stack 
  21. console.log(myObj.stack); 
  22.  
  23. // This will print the following stack to the console: 
  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中,我們使用它來避免向用戶顯示我們是如何實施檢查和斷言本身的不相關的細節。

操作堆棧追蹤實戰

正如我在上一節中提到的,Chai使用堆棧操作技術使堆棧跟蹤更加與我們的用戶相關。下面將揭曉我們是如何做到的。

首先,讓我們來看看當斷言失敗時拋出的AssertionError的構造函數:

  1. // `ssfi` stands for "start stack function". It is the reference to the 
  2. // starting point for removing irrelevant frames from the stack trace 
  3. function AssertionError (message, _props, ssf) { 
  4.   var extend = exclude('name''message''stack''constructor''toJSON'
  5.     , props = extend(_props || {}); 
  6.  
  7.   // Default values 
  8.   this.message = message || 'Unspecified AssertionError'
  9.   this.showDiff = false
  10.  
  11.   // Copy from properties 
  12.   for (var key in props) { 
  13.     this[key] = props[key]; 
  14.   } 
  15.  
  16.   // Here is what is relevant for us: 
  17.   // If a start stack function was provided we capture the current stack trace and pass 
  18.   // it to the `captureStackTrace` function so we can remove frames that come after it 
  19.   ssf = ssf || arguments.callee; 
  20.   if (ssf && Error.captureStackTrace) { 
  21.     Error.captureStackTrace(this, ssf); 
  22.   } else { 
  23.     // If no start stack function was provided we just use the original stack property 
  24.     try { 
  25.       throw new Error(); 
  26.     } catch(e) { 
  27.       this.stack = e.stack; 
  28.     } 
  29.   } 

如你所見,我們使用Error.captureStackTrace捕獲堆棧追蹤并將它存儲在我們正在創建的AssertError實例中(如果存在的話),然后我們將一個起始堆棧函數傳遞給它,以便從堆棧跟蹤中刪除不相關的調用幀,它只顯示Chai的內部實現細節,最終使堆棧變得清晰明了。

現在讓我們來看看@meeber在這個令人驚嘆的PR中提交的代碼。

在你開始看下面的代碼之前,我必須告訴你addChainableMethod方法是干啥的。它將傳遞給它的鏈式方法添加到斷言上,它也用包含斷言的方法標記斷言本身,并將其保存在變量ssfi(啟動堆棧函數指示符)中。這也就意味著當前斷言將會是堆棧中的***一個調用幀,因此我們不會在堆棧中顯示Chai中的任何進一步的內部方法。我沒有添加整個代碼,因為它做了很多事情,有點棘手,但如果你想讀它,點我閱讀。

下面的這個代碼片段中,我們有一個lengOf斷言的邏輯,它檢查一個對象是否有一定的length。我們希望用戶可以像這樣來使用它: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.     // Pay close attention to this line 
  7.     new Assertion(obj, msg, ssfi, true).to.have.property('length'); 
  8.     var len = obj.length; 
  9.  
  10.     // This line is also relevant 
  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); 

Assertion.addChainableMethod('lengthOf', assertLength, assertLengthChain);

在上面的代碼片段中,我突出強調了與我們現在相關的代碼。讓我們從調用this.assert開始說起。

以下是this.assert方法的源代碼:

  1. Assertion.prototype.assert = function (expr, msg, negateMsg, expected, _actual, showDiff) { 
  2.     var ok = util.test(this, arguments); 
  3.     if (false !== showDiff) showDiff = true
  4.     if (undefined === expected && undefined === _actual) showDiff = false
  5.     if (true !== config.showDiff) showDiff = false
  6.  
  7.     if (!ok) { 
  8.         msg = util.getMessage(this, arguments); 
  9.         var actual = util.getActual(this, arguments); 
  10.  
  11.         // This is the relevant line for us 
  12.         throw new AssertionError(msg, { 
  13.                 actual: actual 
  14.             , expected: expected 
  15.             , showDiff: showDiff 
  16.         }, (config.includeStack) ? this.assert : flag(this, 'ssfi')); 
  17.     } 
  18. }; 

assert方法負責檢查斷言布爾表達式是否通過。如果不通過,我們則實例化一個AssertionError。不知道你注意到沒,在實例化AssertionError時,我們也給它傳遞了一個堆棧追蹤函數指示器(ssfi),如果配置的includeStack處于開啟狀態,我們通過將this.assert本身傳遞給它來為用戶顯示整個堆棧跟蹤。反之,我們則只顯示ssfi標記中存儲的內容,隱藏掉堆棧跟蹤中更多的內部實現細節。

現在讓我們來討論下一行和我們相關的代碼吧:

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

As you can see here we are passing the content we’ve got from the ssfi flag when creating our nested assertion. This means that when the new assertion gets created it will use this function as the starting point for removing unuseful frames from the stack trace. By the way, this is the Assertion constructor: 如你所見,我們在創建嵌套斷言時將從ssfi標記中的內容傳遞給了它。這意味著新創建的斷言會使用那個方法作為起始調用幀,從而可以從堆棧追蹤中清除沒有的調用棧。順便也看下Assertion的構造器吧:

  1. function Assertion (obj, msg, ssfi, lockSsfi) { 
  2.     // This is the line that matters to us 
  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標志,這意味著它始終處于堆棧的底部,我們可以刪除它之上的所有調用幀。

通過將ssfi傳遞給嵌套斷言,它只檢查我們的對象是否具有長度屬性,我們就可以避免重置我們將要用作起始指標器的調用幀,然后在堆棧中可以看到以前的addChainableMethod。

這可能看起來有點復雜,所以讓我們回顧一下我們想從棧中刪除無用的調用幀時Chai中所發生的事情:

  • 當我們運行斷言時,我們將它自己的方法作為移除堆棧中的下一個調用幀的參考
  • 斷言失敗時,我們會移除所有我們在參考幀之后保存的內部調用幀。
  • 如果存在嵌套的斷言。我們必須依舊使用當前斷言的父方法作為刪除下一個調用幀的參考點,因此我們把當前的ssfi(起始函數指示器)傳遞給我們所創建的斷言,以便它可以保存。
責任編輯:未麗燕 來源: 碼農網
相關推薦

2017-04-06 14:40:29

JavaScript錯誤處理堆棧追蹤

2017-03-08 08:57:04

JavaScript錯誤堆棧

2021-02-17 11:25:33

前端JavaScriptthis

2017-03-28 21:39:41

ErrorsStack trace代碼

2015-11-04 09:57:18

JavaScript原型

2013-11-05 13:29:04

JavaScriptreplace

2019-11-05 10:03:08

callback回調函數javascript

2024-07-18 10:12:04

2019-03-13 08:00:00

JavaScript作用域前端

2011-09-06 09:56:24

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集群
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产一在线观看 | 日韩国产一区二区 | 中文福利视频 | 久久国产欧美一区二区三区精品 | 国产一二区视频 | 国产精品无码专区在线观看 | 欧美一级在线视频 | 久久综合久久久 | 波多野结衣一区二区 | 日本精品久久久久 | 六月色婷| 男女羞羞视频大全 | 成人乱人乱一区二区三区软件 | 日韩高清中文字幕 | 一区二区免费在线观看 | 国产精品我不卡 | 国产1区2区3区 | 午夜精品久久久久久久 | 欧美精品二区三区 | 亚洲高清在线 | 成年男女免费视频网站 | 精品影院 | 男人天堂99 | 欧美日韩一| 欧美日韩国产在线观看 | 国产欧美一区二区三区在线看 | 欧美片网站免费 | 国产精品久久亚洲7777 | 男人的天堂久久 | 紧缚调教一区二区三区视频 | 精品视频久久久 | 孕妇一级毛片 | 日韩一区二区在线视频 | www.五月天婷婷 | 亚洲一av| 在线观看毛片网站 | 日韩视频中文字幕 | 91电影 | 五月天国产视频 | 久久成人精品视频 | 大香在线伊779 |