原文

JavaScript Errors and Stack Traces in Depth

调用栈Call Stack是如何工作的

栈是一个后进先出LIFO (Last in,First out)的数据结构。调用堆栈实际上就是一个方法列表,按调用顺序保存所有在运行期被调用的方法。调用堆栈会将当前正在执行的函数调用压入堆栈,一旦函数调用结束,又会将它移出堆栈。

console.trace()编写一个简单的例子来演示一下

function c() {
    console.log('c');
    console.trace();
}
function b() {
    console.log('b');
    c();
}
function a() {
    console.log('a');
    b();
}
a();

我们可以看到console输出的结果

console.trace
c @ VM59:3
b @ VM59:7
a @ VM59:11
(anonymous) @ VM59:13

我们调用console.trace()是在c方法里,这个时候c还在执行,并没有返回,因此从console就能看到调用堆栈顶就是c。我们可以稍微改变一下,比如把console.trace()放在b方法里调用,如下:

function c() {
    console.log('c');
}
function b() {
    console.log('b');
    c();
    console.trace();
}
function a() {
    console.log('a');
    b();
}
a();

这时候我们再观察console,就看不到c方法了

VM61:8 console.trace
b @ VM61:8
a @ VM61:13
(anonymous) @ VM61:16

因为console.trace()的调用是发生在了c调用之后,因此这个时候,栈顶c的帧已经出栈,自然就看不到了。

Error对象以及异常处理

通常程序有异常的时候都会有一个Error对象抛出。Error.prototype有以下几种标准属性:

  • constructor

  • message

  • name

更多的,可以翻翻MDN的文档。其中有一个stack属性,要重点关注。尽管这是一个非标准属性,但是绝大多数浏览器都支持这个属性。

一般我们使用try/catch来捕获异常,同时,我们还可以使用finally来做一些清理的工作,因为finally里的代码是一定会执行的。

try {
    console.log('The try block is running...');
} finally {
    try {
        throw new Error('Error inside finally.');
    } catch (err) {
        console.log('Caught an error inside the finally block.');
    }
}

有一个值得探讨的地方,那就是,你可以throw任何数据而不仅仅是一个Error类的实例

function runWithoutThrowing(func) {
    try {
        func();
    } catch (e) {
        console.log('There was an error, but I will not throw it.');
        console.log('The error\'s message was: ' + e.message)
    }
}
function funcThatThrowsString() {
    throw 'I am a String.';
}
runWithoutThrowing(funcThatThrowsString);

这种情况下,e.message的值一定就是undefined了,因为你抛出的并不是一个Error类的实例。

异常还可以作为第一个参数传给callback函数。举个fs.readdir的例子,

const fs = require('fs');
fs.readdir('/example/i-do-not-exist', function callback(err, dirs) {
    if (err instanceof Error) {
        // `readdir` will throw an error because that directory does not exist
        // We will now be able to use the error object passed by it in our callback function
        console.log('Error Message: ' + err.message);
        console.log('See? We can use Errors without using try statements.');
    } else {
        console.log(dirs);
    }
});

处理调用堆栈

思路就两种:

  1. Error.captureStackTrace(NodeJS)

  2. Error.prototype.stack

Error.captureStackTrace是NodeJS提供的一个方法,这个方法会捕捉当前的调用堆栈,然后保存到你指定的对象。

const myObj = {};
function c() {
}
function b() {
    // Here we will store the current stack trace into myObj
    Error.captureStackTrace(myObj);
    c();
}
function a() {
    b();
}
// First we will call these functions
a();
// Now let's see what is the stack trace stored into myObj.stack
console.log(myObj.stack);

另外一种就是利用Error对象的stack属性。但里有个问题,就是你不知道在try/catch里抛出的是什么样的值,这个值它不一定是Error类的实例。不过我们依然能够处理,而且是非常巧妙的进行处理。比如看看Chai这个断言库的AssertionError类的构造函数。

// `ssfi` stands for "start stack function". It is the reference to the
// starting point for removing irrelevant frames from the stack trace
function AssertionError (message, _props, ssf) {
  var extend = exclude('name', 'message', 'stack', 'constructor', 'toJSON')
    , props = extend(_props || {});
  // Default values
  this.message = message || 'Unspecified AssertionError';
  this.showDiff = false;
  // Copy from properties
  for (var key in props) {
    this[key] = props[key];
  }
  // Here is what is relevant for us:
  // If a start stack function was provided we capture the current stack trace and pass
  // it to the `captureStackTrace` function so we can remove frames that come after it
  ssf = ssf || arguments.callee;
  if (ssf && Error.captureStackTrace) {
    Error.captureStackTrace(this, ssf);
  } else {
    // If no start stack function was provided we just use the original stack property
    try {
      throw new Error();
    } catch(e) {
      this.stack = e.stack;
    }
  }
}

参考文档

  1. arguments.callee

  2. arguments.caller

  3. Error


knightuniverse
7 声望0 粉丝