偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

深入理解JavaScript Errors和Stack Traces

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

[[186908]]

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

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

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

一、Stack是如何工作的

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

每當(dāng)有一個函數(shù)調(diào)用,就會將其壓入棧頂。在調(diào)用結(jié)束的時候再將其從棧頂移出。

這種有趣的數(shù)據(jù)結(jié)構(gòu)叫做“***一個進(jìn)入的,將會***個出去”。這就是廣為所知的 LIFO(后進(jìn)先出)。

舉個例子,在函數(shù) x 的內(nèi)部調(diào)用了函數(shù) 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(); 

上面的這段代碼,當(dāng)運(yùn)行 a 的時候,它會被壓到棧頂。然后,當(dāng) b 在 a 中被調(diào)用的時候,它會被繼續(xù)壓入棧頂,當(dāng) c 在 b 中被調(diào)用的時候,也一樣。

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

當(dāng) c 調(diào)用完畢時,它會被從棧頂移出,隨后控制流回到 b。當(dāng) b 執(zhí)行完畢后也會從棧頂移出,控制流交還到 a。***,當(dāng) a 執(zhí)行完畢后也會從棧中移出。

為了更好的展示這樣一種行為,我們用 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(); 

當(dāng)我們在 Node REPL 服務(wù)端執(zhí)行的時候,會返回如下:

  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) 

從上面我們可以看到,當(dāng)棧信息從 c 中打印出來的時候,我看到了 a,b 和 c。現(xiàn)在,如果在 c 執(zhí)行完畢以后,在 b 中把 Stack trace 打印出來,我們可以看到 c 已經(jīng)從棧中移出了,棧中只有 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 已經(jīng)不在棧中了,在其執(zhí)行完以后,從棧中 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) 

概括一下:當(dāng)調(diào)用時,壓入棧頂。當(dāng)它執(zhí)行完畢時,被彈出棧,就是這么簡單。

二、Error 對象和 Error 處理

當(dāng) Error 發(fā)生的時候,通常會拋出一個 Error 對象。Error 對象也可以被看做一個 Error 原型,用戶可以擴(kuò)展其含義,以創(chuàng)建自己的 Error 對象。

Error.prototype 對象通常包含下面屬性:

  • constructor - 一個錯誤實(shí)例原型的構(gòu)造函數(shù)
  • message - 錯誤信息
  • name - 錯誤名稱

這幾個都是標(biāo)準(zhǔn)屬性,有時不同編譯的環(huán)境會有其獨(dú)特的屬性。在一些環(huán)境中,例如 Node 和 Firefox,甚至還有 stack 屬性,這里面包含了錯誤的 Stack trace。一個 Error 的堆棧追蹤包含了從其構(gòu)造函數(shù)開始的所有堆棧幀。

如果你想要學(xué)習(xí)一個 Error 對象的特殊屬性,我強(qiáng)烈建議你看一下在MDN上的這篇文章。

要拋出一個 Error,你必須使用 throw 關(guān)鍵字。為了 catch 一個拋出的 Error,你必須把可能拋出 Error 的代碼用 try 塊包起來。然后緊跟著一個 catch 塊,catch 塊中通常會接受一個包含了錯誤信息的參數(shù)。

和在 Java 中類似,不論在 try 中是否拋出 Error, JavaScript 中都允許你在 try/catch 塊后面緊跟著一個 finally 塊。不論你在 try 中的操作是否生效,在你操作完以后,都用 finally 來清理對象,這是個編程的好習(xí)慣。

介紹到現(xiàn)在的知識,可能對于大部分人來說,都是已經(jīng)掌握了的,那么現(xiàn)在我們就進(jìn)行更深入一些的吧。

使用 try 塊時,后面可以不跟著 catch 塊,但是必須跟著 finally 塊。所以我們就有三種不同形式的 try 語句:

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

Try 語句也可以內(nèi)嵌在一個 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 語句內(nèi)嵌在 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 對象的值。盡管這看起來很炫酷,很靈活,但實(shí)際上這個用法并不好,尤其在一個開發(fā)者改另一個開發(fā)者寫的庫的時候。因?yàn)檫@樣代碼沒有一個標(biāo)準(zhǔn),你不知道其他人會拋出什么信息。這樣的話,你就不能簡單的相信拋出的 Error 信息了,因?yàn)橛锌赡芩⒉皇?Error 信息,而是一個字符串或者一個數(shù)字。另外這也導(dǎo)致了如果你需要處理 Stack trace 或者其他有意義的元數(shù)據(jù),也將變的很困難。

例如給你下面這段代碼:

  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 對象的函數(shù)給 runWithoutThrowing 函數(shù)的話,將***運(yùn)行。然而,如果他拋出一個 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。這現(xiàn)在看起來不是很重要,但是如果你需要確定是否這個 Error 中確實(shí)包含某個屬性,或者用另一種方式處理 Error 的特殊屬性,那你就需要多花很多的功夫了。

另外,當(dāng)拋出一個非 Error 對象的值時,你沒有訪問 Error 對象的一些重要的數(shù)據(jù),比如它的堆棧,而這在一些編譯環(huán)境中是一個非常重要的 Error 對象屬性。

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

  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'將會拋出一個異常,因?yàn)槟夸洸淮嬖?nbsp;
  6.         // 我們可以在我們的回調(diào)函數(shù)中使用 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,那么現(xiàn)在,你們所期待的部分來了:如何使用堆棧追蹤。

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

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

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

  1. const myObj = {}; 
  2.  
  3. function c() { 
  4.  
  5. function b() { 
  6.     // 這里存儲當(dāng)前的堆棧路徑,保存到myObj中 
  7.     Error.captureStackTrace(myObj); 
  8.     c(); 
  9.  
  10. function a() { 
  11.     b(); 
  12.  
  13. // 首先調(diào)用這些函數(shù) 
  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) 

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

好的,那么現(xiàn)在,我們傳遞第二個參數(shù)到 Error.captureStackTrace 看看會發(fā)生什么?

  1. const myObj = {}; 
  2.  
  3. function d() { 
  4.     // 這里存儲當(dāng)前的堆棧路徑,保存到myObj中 
  5.     // 這次我們隱藏包含b在內(nèi)的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. // 首先調(diào)用這些函數(shù) 
  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) 

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

看到這,你可能會問這樣一個問題:“為什么這是有用的呢?”。它之所以有用,是因?yàn)槟憧梢噪[藏所有的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),而這些細(xì)節(jié)其他開發(fā)者調(diào)用的時候并不需要知道。例如,在 Chai 中,我們用這種方法對我們代碼的調(diào)用者屏蔽了不相關(guān)的實(shí)現(xiàn)細(xì)節(jié)。

四、真實(shí)場景中的 Stack Trace 處理

正如我在上一節(jié)中提到的,Chai 用棧處理技術(shù)使得堆棧路徑和調(diào)用者更加相關(guān),這里是我們?nèi)绾螌?shí)現(xiàn)它的。

首先,讓我們來看一下當(dāng)一個 Assertion 失敗的時候,AssertionError 的構(gòu)造函數(shù)做了什么。

  1. // 'ssfi'代表"起始堆棧函數(shù)",它是移除其他不相關(guān)堆棧幀的起始標(biāo)記 
  2. function AssertionError (message, _props, ssf) { 
  3.   var extend = exclude('name', 'message', 'stack', 'constructor', 'toJSON') 
  4.     , props = extend(_props || {}); 
  5.  
  6.   // 默認(rèn)值 
  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.   // 這里是和我們相關(guān)的 
  16.   // 如果提供了起始堆棧函數(shù),那么我們從當(dāng)前堆棧路徑中獲取到, 
  17.   // 并且將其傳遞給'captureStackTrace',以保證移除其后的所有幀 
  18.   ssfssf = ssf || arguments.callee; 
  19.   if (ssf && Error.captureStackTrace) { 
  20.     Error.captureStackTrace(this, ssf); 
  21.   } else { 
  22.     // 如果沒有提供起始堆棧函數(shù),那么使用原始堆棧 
  23.     try { 
  24.       throw new Error(); 
  25.     } catch(e) { 
  26.       this.stack = e.stack; 
  27.     } 
  28.   } 

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

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

在下面的代碼中,展示了 lengthOf 的 Assertion 的邏輯,它是用來檢查一個對象的確定長度的。我們希望調(diào)用我們函數(shù)的開發(fā)者這樣來使用: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.     // 密切關(guān)注這一行 
  7.     new Assertion(obj, msg, ssfi, true).to.have.property('length'); 
  8.     var len = obj.length; 
  9.  
  10.     // 這一行也是相關(guān)的 
  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.     // 密切關(guān)注這一行 
  7.     new Assertion(obj, msg, ssfi, true).to.have.property('length'); 
  8.     var len = obj.length; 
  9.  
  10.     // 這一行也是相關(guān)的 
  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 的布爾表達(dá)式是真還是假。如果是假,則我們必須實(shí)例化一個 AssertionError。這里注意,當(dāng)我們實(shí)例化一個 AssertionError 對象的時候,我們也傳遞了一個起始堆棧函數(shù)指示器(ssfi)。如果配置標(biāo)記 includeStack 是打開的,我們通過傳遞一個 this.assert 給調(diào)用者,以向他展示整個堆棧路徑。可是,如果 includeStack 配置是關(guān)閉的,我們則必須從堆棧路徑中隱藏內(nèi)部實(shí)現(xiàn)細(xì)節(jié),這就需要用到存儲在 ssfi 中的標(biāo)記了。

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

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

可以看到,當(dāng)創(chuàng)建這個內(nèi)嵌 Assertion 的時候,我們傳遞了 ssfi 中已獲取到的內(nèi)容。這意味著,當(dāng)創(chuàng)建一個新的 Assertion 時,將使用這個函數(shù)來作為從堆棧路徑中移除無用堆棧幀的起始點(diǎn)。順便說一下,下面這段代碼是 Assertion 的構(gòu)造函數(shù)。

  1. function Assertion (obj, msg, ssfi, lockSsfi) { 
  2.     // 這是和我們相關(guān)的行 
  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 時說的,它用包含他自己的方法設(shè)置的 ssfi 標(biāo)記,這就意味著這是堆棧路徑中***層的內(nèi)部幀,我們可以移除在它之上的所有幀。

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

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

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

點(diǎn)擊《深入理解 JavaScript Errors 和 Stack Traces》閱讀原文。

【本文是51CTO專欄作者“胡子大哈”的原創(chuàng)文章,轉(zhuǎn)載請聯(lián)系作者本人獲取授權(quán)】

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

責(zé)任編輯:趙寧寧 來源: 51CTO專欄
相關(guān)推薦

2021-02-17 11:25:33

前端JavaScriptthis

2017-04-25 15:30:23

堆棧函數(shù)JavaScript

2015-11-04 09:57:18

JavaScript原型

2019-11-05 10:03:08

callback回調(diào)函數(shù)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-12-16 09:47:01

JavaScript箭頭函數(shù)開發(fā)

2011-03-02 12:33:00

JavaScript

2020-07-24 10:00:00

JavaScript執(zhí)行上下文前端

2012-01-05 15:07:11

JavaScript

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過濾器

2024-05-08 13:52:04

JavaScriptWeb應(yīng)用程序

2012-08-31 10:00:12

Hadoop云計(jì)算群集網(wǎng)絡(luò)

2012-11-08 14:47:52

Hadoop集群

2013-07-31 10:04:42

hadoopHadoop集群集群和網(wǎng)絡(luò)

2024-09-02 14:12:56

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號