有时候人们并不关注这些细节,但这方面的知识肯定有用,尤其是当你正在编写与测试或errors相关的库。例如这个星期我们的chai中出现了一个令人惊叹的Pull Request,它大大改进了我们处理堆栈跟踪的方式,并在用户断言失败时提供了更多的信息。
操作堆栈记录可以让你清理无用数据,并集中精力处理重要事项。此外,当你真正弄清楚Error及其属性,你将会更有信心地利用它。
本文开头部分或许太过于简单,但当你开始处理堆栈记录时,它将变得稍微有些复杂,所以请确保你在开始这个那部分章节之前已经充分理解前面的内容。
堆栈调用如何工作
在谈论errors之前我们必须明白堆栈调用如何工作。它非常简单,但对于我们将要深入的内容而言却是至关重要的。如果你已经知道这部分内容,请随时跳过本节。
每当函数被调用,它都会被推到堆栈的顶部。函数执行完毕,便会从堆栈顶部移除。
这种数据结构的有趣之处在于最后一个入栈的将会第一个从堆栈中移除,这也就是我们所熟悉的LIFO(后进,先出)特性。
这也就是说我们在函数x中调用函数y,那么对应的堆栈中的顺序为x y。
假设你有下面这样的代码:
function c() {
console.log('c');
}
function b() {
console.log('b');
c();
}
function a() {
console.log('a');
b();
}
a();
在上面这里例子中,当执行a函数时,a便会添加到堆栈的顶部,然后当b函数在a函数中被调用,b也会被添加到堆栈的顶部,依次类推,在b中调用c也会发生同样的事情。
当c执行时,堆栈中的函数的顺序为a b c
c执行完毕后便会从栈顶移除,这时控制流重新回到了b中,b执行完毕同样也会从栈顶移除,最后控制流又回到了a中,最后a执行完毕,a也从堆栈中移除。
我们可以利用console.trace()来更好的演示这种行为,它会在控制台打印出当前堆栈中的记录。此外,通常而言你应该从上到下读取堆栈记录。想想下面的每一行代码都是在哪调用的。
function c() {
console.log('c');
console.trace();
}
function b() {
console.log('b');
c();
}
function a() {
console.log('a');
b();
}
a();
在Node REPL服务器上运行上述代码会得到如下结果:
Trace
at c (repl:3:9)
at b (repl:3:1)
at a (repl:3:1)
at repl:1:1 // <-- For now feel free to ignore anything below this point, these are Node's internals
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
at REPLServer.defaultEval (repl.js:313:29)
at bound (domain.js:280:14)
at REPLServer.runBound [as eval] (domain.js:293:12)
如你所见,当我们在c中打印堆栈,堆栈中的记录为a,b,c。
如果我们现在在b中并且在c执行完之后打印堆栈,我们将会发现c已经从堆栈的顶部移除,只剩下了a和b。
function c() {
console.log('c');
}
function b() {
console.log('b');
c();
console.trace();
}
function a() {
console.log('a');
b();
}
a();
正如你看到的那样,堆栈中已经没有c,因为它已经完成运行,已经被弹出去了。
Trace
at b (repl:4:9)
at a (repl:3:1)
(责任编辑:admin)