深入理解JavaScript Errors和Stack Traces.docx
《深入理解JavaScript Errors和Stack Traces.docx》由会员分享,可在线阅读,更多相关《深入理解JavaScript Errors和Stack Traces.docx(15页珍藏版)》请在冰豆网上搜索。
深入理解JavaScriptErrors和StackTraces
深入理解JavaScriptErrors和StackTraces
这次我们聊聊Errors和Stacktraces以及如何熟练地使用它们。
很多同学并不重视这些细节,但是这些知识在你写Testing和Error相关的lib的时候是非常有用的。
使用Stacktraces可以清理无用的数据,让你关注真正重要的问题。
同时,你真正理解Errors和它们的属性到底是什么的时候,你将会更有信心的使用它们。
这篇文章在开始的时候看起来比较简单,但当你熟练运用Stacktrace以后则会感到非常复杂。
所以在看难的章节之前,请确保你理解了前面的内容。
一、Stack是如何工作的
在我们谈到Errors之前,我们必须理解Stack是如何工作的。
它其实非常简单,但是在开始之前了解它也是非常必要的。
如果你已经知道了这些,可以略过这一章节。
每当有一个函数调用,就会将其压入栈顶。
在调用结束的时候再将其从栈顶移出。
这种有趣的数据结构叫做“最后一个进入的,将会第一个出去”。
这就是广为所知的LIFO(后进先出)。
举个例子,在函数x的内部调用了函数y,这时栈中就有个顺序先x后y。
我再举另外一个例子,看下面代码:
1.function c() {
2. console.log('c');
3.}
4.
5.function b() {
6. console.log('b');
7. c();
8.}
9.
10.function a() {
11. console.log('a');
12. b();
13.}
14.
15.a();
上面的这段代码,当运行a的时候,它会被压到栈顶。
然后,当b在a中被调用的时候,它会被继续压入栈顶,当c在b中被调用的时候,也一样。
在运行c的时候,栈中包含了a,b,c,并且其顺序也是a,b,c。
当c调用完毕时,它会被从栈顶移出,随后控制流回到b。
当b执行完毕后也会从栈顶移出,控制流交还到a。
最后,当a执行完毕后也会从栈中移出。
为了更好的展示这样一种行为,我们用console.trace()来将Stacktrace打印到控制台上来。
通常我们读Stacktraces信息的时候是从上往下读的。
1.function c() {
2. console.log('c');
3. console.trace();
4.}
5.
6.function b() {
7. console.log('b');
8. c();
9.}
10.
11.function a() {
12. console.log('a');
13. b();
14.}
15.
16.a();
当我们在NodeREPL服务端执行的时候,会返回如下:
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。
现在,如果在c执行完毕以后,在b中把Stacktrace打印出来,我们可以看到c已经从栈中移出了,栈中只有a和b。
1.function c() {
2. console.log('c');
3.}
4.
5.function b() {
6. console.log('b');
7. c();
8. console.trace();
9.}
10.
11.function a() {
12. console.log('a');
13. b();
14.}
15.
16.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属性,这里面包含了错误的Stacktrace。
一个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.');
11.}
你也可以把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. }
10.}
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. }
9.}
这里给出另外一个重要的提示:
你可以抛出非Error对象的值。
尽管这看起来很炫酷,很灵活,但实际上这个用法并不好,尤其在一个开发者改另一个开发者写的库的时候。
因为这样代码没有一个标准,你不知道其他人会抛出什么信息。
这样的话,你就不能简单的相信抛出的Error信息了,因为有可能它并不是Error信息,而是一个字符串或者一个数字。
另外这也导致了如果你需要处理Stacktrace或者其他有意义的元数据,也将变的很困难。
例如给你下面这段代码:
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.
10.function funcThatThrowsError() {
11. throw new TypeError('I am a TypeError.');
12.}
13.
14.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.
10.function funcThatThrowsString() {
11. throw 'I am a String.';
12.}
13.
14.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对象,这使得处理promisereject变得很简单。
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.});
三、使用StackTrace
ok,那么现在,你们所期待的部分来了:
如何使用堆栈追踪。
这一章专门讨论支持Error.captureStackTrace的环境,如:
NodeJS。
Error.captureStackTrace函数的第一个参数是一个object对象,第二个参数是一个可选的function。
捕获堆栈跟踪所做的是要捕获当前堆栈的路径(这是显而易见的),并且在object对象上创建一个stack属性来存储它。
如果提供了第二个function参数,那么这个被传递的函数将会被看成是本次堆栈调用的终点,本次堆栈跟踪只会展示到这个函数被调用之前。
我们来用几个例子来更清晰的解释下。
我们将捕获当前堆栈路径并且将其存储到一个普通object对象中。
1.const myObj = {};
2.
3.function c() {
4.}
5.
6.function b() {
7. // 这里存储当前的堆栈路径,保存到myObj中
8. Error.captureStackTrace(myObj);
9. c();
10.}
11.
12.function a() {
13. b();
14.}
15.
16.// 首先调用这些函数
17.a();
18.
19.// 这里,我们看一下堆栈路径往 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 last entry in the stack
24.// at a (repl:
2:
1)
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)
我们从上面的例子中可以看到,我们首先调用了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.
9.function c() {
10. d();
11.}
12.
13.function b() {
14. c();
15.}
16.
17.function a() {
18. b();
19.}
20.
21.// 首先调用这些函数
22.a();
23.
24.// 这里,我们看一下堆栈路径往 myObj.stack 中存储了什么
25.console.log(myObj.stack);
26.
27.// 这里将会打印如下堆栈信息到控制台
28.// at 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.// 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.defaultEval (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.// at emitOne (events.js:
101:
20)
当我们传递b到Error.captureStackTraceFunction里时,它隐藏了b和在它以上的所有堆栈帧。
这就是为什么堆栈路径里只有a的原因。
看到这,你可能会问这样一个问题:
“为什么这是有用的呢?
”。
它之所以有用,是因为你可以隐藏所有的内部实现细节,而这些细节其他开发者调用的时候并不需要知道。
例如,在Chai中,我们用这种方法对我们代码的调用者屏蔽了不相关的实现细节。
四、真实场景中的StackTrace处理
正如我在上一节中提到的,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.