版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、深入理解JavaScript Errors和Stack Traces这次我们聊聊 Errors 和 Stack traces 以及如何熟练地使用它们。很多同学并不重视这些细节,但是这些知识在你写 Testing 和 Error 相关的 lib 的时候是非常有用的。使用 Stack traces 可以清理无用的数据,让你关注真正重要的问题。同时,你真正理解 Errors 和它们的属性到底是什么的时候,你将会更有信心的使用它们。这篇文章在开始的时候看起来比较简单,但当你熟练运用 Stack trace 以后则会感到非常复杂。所以在看难的章节之前,请确保你理解了前面的内容。一、Stack是如何工作的
2、在我们谈到 Errors 之前,我们必须理解 Stack 是如何工作的。它其实非常简单,但是在开始之前了解它也是非常必要的。如果你已经知道了这些,可以略过这一章节。每当有一个函数调用,就会将其压入栈顶。在调用结束的时候再将其从栈顶移出。这种有趣的数据结构叫做“最后一个进入的,将会第一个出去”。这就是广为所知的 LIFO(后进先出)。举个例子,在函数 x 的内部调用了函数 y,这时栈中就有个顺序先 x 后 y。我再举另外一个例子,看下面代码:1. function c() 2. console.log('c&
3、#39;); 3. 4. 5. function b() 6. console.log('b'); 7. c(); 8. 9. 10. function a() 11. console.log('a'); 12. b();&
4、#160;13. 14. 15. a(); 上面的这段代码,当运行 a 的时候,它会被压到栈顶。然后,当 b 在 a 中被调用的时候,它会被继续压入栈顶,当 c 在 b 中被调用的时候,也一样。在运行 c 的时候,栈中包含了 a,b,c,并且其顺序也是 a,b,c。当 c 调用完毕时,它会被从栈顶移出,随后控制流回到 b。当 b 执行完毕后也会从栈顶移出,控制流交还到 a。最后,当 a 执行完毕后也会从栈中移出。为了更好的展示这样一种行为,我们用 console.trace() 来将 Stack trace 打印到控制台上来。通常我们读 Stack trace
5、s 信息的时候是从上往下读的。1. function c() 2. console.log('c'); 3. console.trace(); 4. 5. 6. function b() 7. console.log('b'); 8. c(); 9
6、. 10. 11. function a() 12. console.log('a'); 13. b(); 14. 15. 16. a(); 当我们在 Node REPL 服务端执行的时候,会返回如下:1. Trace 2. at c (repl:3:9) 3. &
7、#160; 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'
8、;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.
9、0; 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。现在,如果在 c 执行完毕以
10、后,在 b 中把 Stack trace 打印出来,我们可以看到 c 已经从栈中移出了,栈中只有 a 和 b。1. function c() 2. console.log('c'); 3. 4. 5. function b() 6. console.log('b'); 7. c(); 8. &
11、#160; console.trace(); 9. 10. 11. function a() 12. console.log('a'); 13. b(); 14. 15. 16. a(); 下面可以看到,c 已经不在栈中了,在其执行完以后,从栈中 pop 出去了。1. Trace 2.
12、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
13、60;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.
14、60; 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 (
15、repl.js:513:10) 概括一下:当调用时,压入栈顶。当它执行完毕时,被弹出栈,就是这么简单。二、Error 对象和 Error 处理当 Error 发生的时候,通常会抛出一个 Error 对象。Error 对象也可以被看做一个 Error 原型,用户可以扩展其含义,以创建自己的 Error 对象。Etotype 对象通常包含下面属性:· constructor - 一个错误实例原型的构造函数· message - 错误信息· name - 错误名称这几个都是标准属性,有时不同编译的环境会有其独特的属性。在一些环境中,例如 Nod
16、e 和 Firefox,甚至还有 stack 属性,这里面包含了错误的 Stack trace。一个 Error 的堆栈追踪包含了从其构造函数开始的所有堆栈帧。如果你想要学习一个 Error 对象的特殊属性,我强烈建议你看一下在MDN上的这篇文章。要抛出一个 Error,你必须使用 throw 关键字。为了 catch 一个抛出的 Error,你必须把可能抛出 Error 的代码用 try 块包起来。然后紧跟着一个 catch 块,catch 块中通常会接受一个包含了错误信息的参数。和在 Java 中类似,不论在 try 中是否抛出 Error, JavaScript 中都允许你在 try/c
17、atch 块后面紧跟着一个 finally 块。不论你在 try 中的操作是否生效,在你操作完以后,都用 finally 来清理对象,这是个编程的好习惯。介绍到现在的知识,可能对于大部分人来说,都是已经掌握了的,那么现在我们就进行更深入一些的吧。使用 try 块时,后面可以不跟着 catch 块,但是必须跟着 finally 块。所以我们就有三种不同形式的 try 语句:· try.catch· try.finally· try.catch.finallyTry 语句也可以内嵌在一个 try 语句中,如:1. try 2.
18、60; try 3. / 这里抛出的Error,将被下面的catch获取到 4. throw new Error('Nested error.'); 5. catch (nestedErr)
19、160;6. / 这里会打印出来 7. console.log('Nested catch'); 8. 9. catch (err) 10. console.log('This wi
20、ll not run.'); 11. 你也可以把 try 语句内嵌在 catch 和 finally 块中:1. try 2. throw new Error('First error'); 3. catch (err) 4. console.log('First catch running')
21、; 5. try 6. throw new Error('Second error'); 7. catch (nestedErr) 8. console.log('Seco
22、nd catch running.'); 9. 10. 1. try 2. console.log('The try block is running.'); 3. finally 4. try 5.
23、 throw new Error('Error inside finally.'); 6. catch (err) 7. console.log('Caught an error inside the finally block.&
24、#39;); 8. 9. 这里给出另外一个重要的提示:你可以抛出非 Error 对象的值。尽管这看起来很炫酷,很灵活,但实际上这个用法并不好,尤其在一个开发者改另一个开发者写的库的时候。因为这样代码没有一个标准,你不知道其他人会抛出什么信息。这样的话,你就不能简单的相信抛出的 Error 信息了,因为有可能它并不是 Error 信息,而是一个字符串或者一个数字。另外这也导致了如果你需要处理 Stack trace 或者其他有意义的元数据,也将变的很困难。例如给你下面这段代码:1. function ru
25、nWithoutThrowing(func) 2. try 3. func(); 4. catch (e) 5. console.log('There was an error
26、, but I will not throw it.'); 6. console.log('The error's message was: ' + e.message) 7. 8. 9. 10. function funcThatThr
27、owsError() 11. throw new TypeError('I am a TypeError.'); 12. 13. 14. runWithoutThrowing(funcThatThrowsError); 这段代码,如果其他人传递一个带有抛出 Error 对象的函数给 runWithoutThrowing 函数的话,将完美运行。然而,如果他抛出一个 String 类型的话,则情况就麻烦了。1. func
28、tion runWithoutThrowing(func) 2. try 3. func(); 4. catch (e) 5. console.log('There was a
29、n error, but I will not throw it.'); 6. console.log('The error's message was: ' + e.message) 7. 8. 9. 10. function
30、;funcThatThrowsString() 11. throw 'I am a String.' 12. 13. 14. runWithoutThrowing(funcThatThrowsString); 可以看到这段代码中,第二个 console.log 会告诉你这个 Error 信息是 undefined。这现在看起来不是很重要,但是如果你需要确定是否这个 Error 中确实包含某个属性,或者用另一种方式处理 Erro
31、r 的特殊属性,那你就需要多花很多的功夫了。另外,当抛出一个非 Error 对象的值时,你没有访问 Error 对象的一些重要的数据,比如它的堆栈,而这在一些编译环境中是一个非常重要的 Error 对象属性。Error 还可以当做其他普通对象一样使用,你并不需要抛出它。这就是为什么它通常作为回调函数的第一个参数,就像 fs.readdir 函数这样:1. const fs = require('fs'); 2. 3. fs.readdir('/example/i-do-not-exist', func
32、tion callback(err, dirs) 4. if (err instanceof Error) 5. / 'readdir'将会抛出一个异常,因为目录不存在 6. / 我们可以在我们的回调函数中使用
33、;Error 对象 7. console.log('Error Message: ' + err.message); 8. console.log('See? We can use Errors without using
34、0;try statements.'); 9. else 10. console.log(dirs); 11. 12. ); 最后,你也可以在 promise 被 reject 的时候使用 Error 对象,这使得处理 promise reject 变得很简单。1. new Promise(functi
35、on(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.
36、 if (err instanceof Error) 7. console.log('The promise was rejected with an error.'); 8. console.log('Error&
37、#160;Message: ' + err.message); 9. 10. ); 三、使用 Stack Traceok,那么现在,你们所期待的部分来了:如何使用堆栈追踪。这一章专门讨论支持 Error.captureStackTrace 的环境,如:NodeJS。Error.captureStackTrace 函数的第一个参数是一个 object 对象,第二个参数是一个可选的 function。捕获堆栈跟踪所做的是要捕获当前堆栈的路径(这是显而易见的),并且在 object
38、对象上创建一个 stack 属性来存储它。如果提供了第二个 function 参数,那么这个被传递的函数将会被看成是本次堆栈调用的终点,本次堆栈跟踪只会展示到这个函数被调用之前。我们来用几个例子来更清晰的解释下。我们将捕获当前堆栈路径并且将其存储到一个普通 object 对象中。1. const myObj = 2. 3. function c() 4. 5. 6. function b() 7. /
39、60;这里存储当前的堆栈路径,保存到myObj中 8. Error.captureStackTrace(myObj); 9. c(); 10. 11. 12. function a() 13. b(); 14. 15. 16. / 首先调用这些函数 17. a(); 18. 19. /
40、0;这里,我们看一下堆栈路径往 myObj.stack 中存储了什么 20. console.log(myObj.stack); 21. 22. / 这里将会打印如下堆栈信息到控制台 23. / at b (repl:3:7) <- Since it was called inside B, the B call is the
41、0;last entry in the stack 24. / at a (repl:2:1) 25. / at repl:1:1 <- Node internals below this line 26. / at realRunInThisContextScript (v
42、m.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. /
43、60; at bound (domain.js:280:14) 31. / at REPLServer.runBound as eval (domain.js:293:12) 32. / at REPLServer.onLine (repl.js:513:10) 我们从上面的例子中可以看到,我们首先调用了a(a被压入栈),然后从a的内部调用了b(b被压入栈,并且在a的上面)
44、。在b中,我们捕获到了当前堆栈路径并且将其存储在了 myObj 中。这就是为什么打印在控制台上的只有a和b,而且是下面a上面b。好的,那么现在,我们传递第二个参数到 Error.captureStackTrace 看看会发生什么?1. const myObj = 2. 3. function d() 4. / 这里存储当前的堆栈路径,保存到myObj中 5. / 这次我们隐藏包含b在内
45、的b以后的所有堆栈帧 6. Error.captureStackTrace(myObj, b); 7. 8. 9. function c() 10. d(); 11. 12. 13. function b() 14. c(); 15. 16. 17. function&
46、#160;a() 18. b(); 19. 20. 21. / 首先调用这些函数 22. a(); 23. 24. / 这里,我们看一下堆栈路径往 myObj.stack 中存储了什么 25. console.log(myObj.stack); 26. 27. / 这里将会打印如下堆栈信息到控制台 28. / at
47、;a (repl:2:1) <- As you can see here we only get frames before b was called 29. / at repl:1:1 <- Node internals below this line 30. /
48、 at realRunInThisContextScript (vm.js:22:35) 31. / at sigintHandlersWrap (vm.js:98:12) 32. / at ContextifyScript.Script.runInThisContext (vm.js:24:12) 33. / at REPLServer.defau
49、ltEval (repl.js:313:29) 34. / at bound (domain.js:280:14) 35. / at REPLServer.runBound as eval (domain.js:293:12) 36. / at REPLServer.onLine (repl.js:513:10) 37. /
50、60; at emitOne (events.js:101:20) 当我们传递 b 到 Error.captureStackTraceFunction 里时,它隐藏了 b 和在它以上的所有堆栈帧。这就是为什么堆栈路径里只有a的原因。看到这,你可能会问这样一个问题:“为什么这是有用的呢?”。它之所以有用,是因为你可以隐藏所有的内部实现细节,而这些细节其他开发者调用的时候并不需要知道。例如,在 Chai 中,我们用这种方法对我们代码的调用者屏蔽了不相关的实现细节。四、真实场景中的 Stack Trace 处理正如我在上一节中提到的,C
51、hai 用栈处理技术使得堆栈路径和调用者更加相关,这里是我们如何实现它的。首先,让我们来看一下当一个 Assertion 失败的时候,AssertionError 的构造函数做了什么。1. / 'ssfi'代表"起始堆栈函数",它是移除其他不相关堆栈帧的起始标记 2. function AssertionError (message, _props, ssf) 3. var extend = exclude('nam
52、e', 'message', 'stack', 'constructor', 'toJSON') 4. , props = extend(_props | ); 5. 6. / 默认值 7. this.message = message | '
53、;Unspecified AssertionError' 8. this.showDiff = false; 9. 10. / 从属性中copy 11. for (var key in props) 12. thiskey = propskey; 13. 14.
54、 15. / 这里是和我们相关的 16. / 如果提供了起始堆栈函数,那么我们从当前堆栈路径中获取到, 17. / 并且将其传递给'captureStackTrace',以保证移除其后的所有帧 18. ssfssf = ssf | arguments.callee; 19. if (ssf &&
55、;Error.captureStackTrace) 20. Error.captureStackTrace(this, ssf); 21. else 22. / 如果没有提供起始堆栈函数,那么使用原始堆栈 23. try 24. throw&
56、#160;new Error(); 25. catch(e) 26. this.stack = e.stack; 27. 28. 29. 正如你在上面可以看到的,我们使用了 Error.captureStackTrace 来捕获堆栈路径,并且把它存储在我们所创建的一个 Assertion
57、Error 实例中。然后传递了一个起始堆栈函数进去(用if判断如果存在则传递),这样就从堆栈路径中移除掉了不相关的堆栈帧,不显示一些内部实现细节,保证了堆栈信息的“清洁”。在我们继续看下面的代码之前,我要先告诉你 addChainableMethod 都做了什么。它添加所传递的可以被链式调用的方法到 Assertion,并且用包含了 Assertion 的方法标记 Assertion 本身。用ssfi(表示起始堆栈函数指示器)这个名字记录。这意味着当前 Assertion 就是堆栈的最后一帧,就是说不会再多显示任何 Chai 项目中的内部实现细节了。我在这里就不多列出来其整个代码了,里面用了很
58、多 trick 的方法,但是如果你想了解更多,可以从 这个链接 里获取到。在下面的代码中,展示了 lengthOf 的 Assertion 的逻辑,它是用来检查一个对象的确定长度的。我们希望调用我们函数的开发者这样来使用:expect('foo', 'bar').to.have.lengthOf(2)。1. function assertLength (n, msg) 2. if (msg) flag(this, 'messa
59、ge', msg); 3. var obj = flag(this, 'object') 4. , ssfi = flag(this, 'ssfi'); 5. 6. / 密切关注这一行 7.
60、 new Assertion(obj, msg, ssfi, true).perty('length'); 8. var len = obj.length; 9. 10. / 这一行也是相关的 11. this.assert( 12.
61、160; len = n 13. , 'expected #this to have a length of #exp but got #act' 14.
62、160; , 'expected #this to not have a length of #act' 15. , n 16. , len 17. ); 18. 19.
63、 20. Assertion.addChainableMethod('lengthOf', assertLength, assertLengthChain); 下面是 this.assert 方法的代码:1. function assertLength (n, msg) 2. if (msg) flag(this, 'message', msg); 3.
64、0; var obj = flag(this, 'object') 4. , ssfi = flag(this, 'ssfi'); 5. 6. / 密切关注这一行 7. new Assertion(obj,
65、60;msg, ssfi, true).perty('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
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025单位向个人的汽车租赁合同
- 课题申报书:指向学生个性创造力发展的阶位课程设计研究
- 2025软件测试合同范本
- 2025工程设备采购合同书
- 2025路灯购销合同
- 2025电力工程设备招标程序及招标文件范本第Ⅳ部分(招标文件第2卷合同条款)
- 2025百货购销合同范文
- 棉麻毛初加工设备投资规划项目建议书
- 超硬材料生产加工项目可行性研究报告
- 双线性滤波器优化算法-洞察分析
- 2024-2025学年人教版八年级上册数学期末押题卷(含答案)
- 高标准农田建设的风险管理与应急预案
- 衡重式及重力式挡土墙自动计算表
- 2024年01月11129土木工程力学(本)期末试题答案
- 家政公司员工合同范例
- 2024年官方兽医考试题库及参考答案
- 浙江财经大学《政治经济学》2021-2022学年第一学期期末试卷
- 山东省济南市2023-2024学年高二上学期期末考试物理试题 附答案
- 2024年度太阳能光伏设备购销合同3篇
- NB/T 11127-2023在用钢丝绳芯输送带报废检测技术规范
- 2024年GYB创业意识及就业能力知识考试题库(附含答案)
评论
0/150
提交评论