在我们日常开发 JavaScript 的调试过程中,控制台常常需要打印出各种 Error。就像下图:

那么他是什么呢?他其实是就是抛出 Error 时的 Stack traces。
什么是 Stack
在我们谈到 Errors 之前,我们必须理解 Stack 是如何工作的。它其实非常简单,但是在开始之前了解它也是非常必要的。如果你已经知道了这些,可以略过这一章节。
JS运行时的执行栈概念,每当有一个函数调用,就会将其压入栈顶。在调用结束的时候再将其从栈顶移出。这种有趣的数据结构叫做“最后一个进入的,将会第一个出去”。这就是广为所知的 LIFO(后进先出)。当调用时,压入栈顶。当它执行完毕时,被弹出栈,就是这么简单。
Error 对象和 Error 处理
当 Error 发生的时候,通常会抛出一个 Error 对象。Error 对象也可以被看做一个 Error 原型,用户可以扩展其含义,并创建自己的 Error 对象。
Error.prototype 对象通常包含下面属性:
- constructor - 一个错误实例原型的构造函数
- message - 错误信息
- name - 错误名称
这几个都是标准属性,有时不同编译的环境会有其独特的属性。在一些环境中,例如 Node 、Chrome,甚至还有 stack 属性,这里面包含了错误的 Stack trace。一个 Error 的堆栈追踪包含了从其构造函数开始的所有堆栈帧。
这里只是简单的介绍了 Error 对象的,如果想了解具体的 Error 可以查看相关文档
要抛出一个 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
虽然你可以抛出非 Error 的对象值,看起来很牛逼,然并没有什么卵用,还会造成不必要的麻烦。尤其在一个开发者改另一个开发者写的库的时候。因为这样代码没有一个标准,你不知道其他人会抛出什么信息。这样的话,你就不能简单的相信抛出的 Error 信息了,因为有可能它并不是 Error 信息,而是一个字符串或者一个数字。
使用 Stack Trace
对 Error 有了基本了解后,我们来看看如何使用 Stack Trace 。
Error.captureStackTrace
Error.captureStackTrace 函数的第一个参数是一个 object 对象,第二个参数是一个可选的 function。捕获堆栈跟踪所做的是要捕获当前堆栈的路径,并将它存储到 object 对象上的 stack 属性。如果提供了 function 参数,那么这个被传递的函数将会被看成是本次堆栈调用的终点,本次堆栈跟踪只会展示这个函数之前的调用。
我们来看一段代码事例:
const shopee = {};function c() {}function b() {Error.captureStackTrace(shopee);c();}function a() {b();}a();console.log(shopee.stack);
output:
**
我们从上面的例子中可以看到,我们首先调用了a(a被压入栈),然后从a的内部调用了b(b被压入栈,并且在a的上面)。在b中,我们捕获到了当前堆栈路径并且将其存储在了 myObj 中。这就是为什么打印在控制台上的只有a和b,而且是下面a上面b。
好的,那么现在,我们传递第二个参数到 Error.captureStackTrace 看看会发生什么?
const shopee = {};function d() {Error.captureStackTrace(shopee, c);}function c() {d();}function b() {c();}function a() {b();}a();console.log(shopee.stack);
output:

当我们传递 c 到 Error.captureStackTraceFunction 里时,它隐藏了 c 和在它以上的所有堆栈帧。这就是为什么堆栈路径里只有a的原因。
这有什么用呢?它可以隐藏所有的内部实现细节,而这些细节其他开发者调用的时候并不需要知道。
实际使用
我们在做前端错误上报的时候可以上报报错的 stack,有助于我们定位具体的代码执行环境。还可以指定输出截止的堆栈帧。封装了一个函数:
function MyError (message, errObj, ssf) {// 给定一个默认值this.message = message || 'is Error';// 从属性中复制一份for (var key in errObj) {this[key] = errObj[key];}// 如果提供了起始堆栈函数,那么我们从当前堆栈路径中获取到,// 并且将其传递给'captureStackTrace',以保证移除其后的所有帧ssf = ssf || arguments.callee;if (ssf && Error.captureStackTrace) {Error.captureStackTrace(this, ssf);} else {// 如果没有提供起始堆栈函数,那么使用原始堆栈try {throw new Error();} catch(e) {this.stack = e.stack;}}}
