大多数工程师可能并没留意过 JS 中错误对象、错误堆栈的细节,即使他们每天的日常工作会面临不少的报错,部分同学甚至在 console 的错误面前一脸懵逼,不知道从何开始排查,如果你对本文讲解的内容有系统的了解,就会从容很多。而错误堆栈清理能让你有效去掉噪音信息,聚焦在真正重要的地方,此外,如果理解了 Error 的各种属性到底是什么,你就能更好的利用他。

接下来,我们就直奔主题。

调用栈的工作机制

在探讨 JS 中的错误之前,我们必须理解调用栈(Call Stack)的工作机制,其实这个机制非常简单,如果你对这个已经一清二楚了,可以直接跳过这部分内容。

简单的说:函数被调用时,就会被加入到调用栈顶部,执行结束之后,就会从调用栈顶部移除该函数,这种数据结构的关键在于后进先出,即大家所熟知的 LIFO。比如,当我们在函数 y 内部调用函数 x 的时候,调用栈从下往上的顺序就是 y -> x 。

我们再举个代码实例:

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

这段代码运行时,首先 a 会被加入到调用栈的顶部,然后,因为 a 内部调用了 b,紧接着 b 被加入到调用栈的顶部,当 b 内部调用 c 的时候也是类似的。在调用 c的时候,我们的调用栈从下往上会是这样的顺序:a -> b -> c。在 c 执行完毕之后,c 被从调用栈中移除,控制流回到 b 上,调用栈会变成:a -> b,然后 b 执行完之后,调用栈会变成:a,当 a 执行完,也会被从调用栈移除。

为了更好的说明调用栈的工作机制,我们对上面的代码稍作改动,使用 console.trace 来把当前的调用栈输出到 console 中,你可以认为console.trace 打印出来的调用栈的每一行出现的原因是它下面的那行调用而引起的。

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

当我们在 Node.js 的 REPL 中运行这段代码,会得到如下的结果:

Traceat c (repl:3:9)at b (repl:3:1)at a (repl:3:1)at repl:1:1 // <-- 从这行往下的内容可以忽略,因为这些都是 Node 内部的东西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 内部调用 console.trace 的时候,调用栈从下往上的结构是:a -> b -> c。如果把代码再稍作改动,在 b 中 c 执行完之后调用,如下:

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

通过输出结果可以看到,此时打印的调用栈从下往上是:a -> b,已经没有 c 了,因为 c 执行完之后就从调用栈移除了。

Traceat b (repl:4:9)at a (repl:3:1)at repl:1:1  // <-- 从这行往下的内容可以忽略,因为这些都是 Node 内部的东西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)at REPLServer.onLine (repl.js:513:10)

再总结下调用栈的工作机制:调用函数的时候,会被推到调用栈的顶部,而执行完毕之后,就会从调用栈移除。

Error 对象及错误处理

当代码中发生错误时,我们通常会抛出一个 Error 对象。Error 对象可以作为扩展和创建自定义错误类型的原型。Error 对象的 prototype 具有以下属性:

  • constructor – 负责该实例的原型构造函数;
  • message – 错误信息;
  • name – 错误的名字;

上面都是标准属性,有些 JS 运行环境还提供了标准属性之外的属性,如 Node.js、Firefox、Chrome、Edge、IE 10、Opera 和 Safari 6+ 中会有 stack 属性,它包含了错误代码的调用栈,接下来我们简称错误堆栈。错误堆栈包含了产生该错误时完整的调用栈信息。如果您想了解更多关于 Error 对象的非标准属性,我强烈建议你阅读 MDN 的这篇文章。

抛出错误时,你必须使用 throw 关键字。为了捕获抛出的错误,则必须使用 try catch 语句把可能出错的代码块包起来,catch 的时候可以接收一个参数,该参数就是被抛出的错误。与 Java 中类似,JS 中也可以在 try catch 语句之后有 finally,不论前面代码是否抛出错误 finally 里面的代码都会执行,这种语言的常见用途有:在 finally 中做些清理的工作。

此外,你可以使用没有 catch 的 try 语句,但是后面必须跟上 finally,这意味着我们可以使用三种不同形式的 try 语句:

  • try … catch
  • try … finally
  • try … catch … finally

try 语句还可以嵌套在 try 语句中,比如:

try {try {throw new Error('Nested error.'); // 这里的错误会被自己紧接着的 catch 捕获} catch (nestedErr) {console.log('Nested catch'); // 这里会运行}
} catch (err) {console.log('This will not run.');  // 这里不会运行
}

try 语句也可以嵌套在 catch 和 finally 语句中,比如下面的两个例子:

try {throw new Error('First error');
} catch (err) {console.log('First catch running');try {throw new Error('Second error');} catch (nestedErr) {console.log('Second catch running.');}
}
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.');}
}

同样需要注意的是,你可以抛出不是 Error 对象的任意值。这可能看起来很酷,但在工程上却是强烈不建议的做法。如果恰巧你需要处理错误的调用栈信息和其他有意义的元数据,抛出非 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 funcThatThrowsError() {throw new TypeError('I am a TypeError.');
}runWithoutThrowing(funcThatThrowsError);

如果 runWithoutThrowing 的调用者传入的函数都能抛出 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);

这段代码运行时,runWithoutThrowing 中的第 2 次 console.log 会抛出错误,因为 e.message 是未定义的。这些看起来似乎没什么大不了的,但如果你的代码需要使用 Error 对象的某些特定属性,那么你就需要做很多额外的工作来确保一切正常。如果你抛出的值不是 Error 对象,你就不会拿到错误相关的重要信息,比如 stack,虽然这个属性在部分 JS 运行环境中才会有。

Error 对象也可以向其他对象那样使用,你可以不用抛出错误,而只是把错误传递出去,Node.js 中的错误优先回调就是这种做法的典型范例,比如 Node.js 中的 fs.readdir 函数:

const fs = require('fs');fs.readdir('/example/i-do-not-exist', function callback(err, dirs) {if (err) {// `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 functionconsole.log('Error Message: ' + err.message);console.log('See? We can use Errors without using try statements.');} else {console.log(dirs);}
});

此外,Error 对象还可以用于 Promise.reject 的时候,这样可以更容易的处理 Promise 失败,比如下面的例子:

new Promise(function(resolve, reject) {reject(new Error('The promise was rejected.'));
}).then(function() {console.log('I am an error.');
}).catch(function(err) {if (err instanceof Error) {console.log('The promise was rejected with an error.');console.log('Error Message: ' + err.message);}
});

错误堆栈的裁剪

Node.js 才支持这个特性,通过 Error.captureStackTrace 来实现,Error.captureStackTrace 接收一个 object 作为第 1 个参数,以及可选的 function 作为第 2 个参数。其作用是捕获当前的调用栈并对其进行裁剪,捕获到的调用栈会记录在第 1 个参数的 stack 属性上,裁剪的参照点是第 2 个参数,也就是说,此函数之前的调用会被记录到调用栈上面,而之后的不会。

让我们用代码来说明,首先,把当前的调用栈捕获并放到 myObj 上:

const myObj = {};function c() {
}function b() {// 把当前调用栈写到 myObj 上Error.captureStackTrace(myObj);c();
}function a() {b();
}// 调用函数 a
a();// 打印 myObj.stack
console.log(myObj.stack);// 输出会是这样
//    at b (repl:3:7) <-- Since it was called inside B, the B call is the last entry in the stack
//    at a (repl:2:1)
//    at repl:1:1 <-- Node internals below this line
//    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)
//    at REPLServer.onLine (repl.js:513:10)

上面的调用栈中只有 a -> b,因为我们在 b 调用 c 之前就捕获了调用栈。现在对上面的代码稍作修改,然后看看会发生什么:

const myObj = {};function d() {// 我们把当前调用栈存储到 myObj 上,但是会去掉 b 和 b 之后的部分Error.captureStackTrace(myObj, b);
}function c() {d();
}function b() {c();
}function a() {b();
}// 执行代码
a();// 打印 myObj.stack
console.log(myObj.stack);// 输出如下
//    at a (repl:2:1) <-- As you can see here we only get frames before `b` was called
//    at repl:1:1 <-- Node internals below this line
//    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)
//    at REPLServer.onLine (repl.js:513:10)
//    at emitOne (events.js:101:20)

在这段代码里面,因为我们在调用 Error.captureStackTrace 的时候传入了 b,这样 b 之后的调用栈都会被隐藏。

现在你可能会问,知道这些到底有啥用?如果你想对用户隐藏跟他业务无关的错误堆栈(比如某个库的内部实现)就可以试用这个技巧。

总结

通过本文的描述,相信你对 JS 中的调用栈、Error 对象、错误堆栈有了清晰的认识,在遇到错误的时候不在慌乱。如果对文中的内容有任何疑问,欢迎在下面评论。

One More Thing

想知道这个人以后还会写什么?请关注本专栏,或者关注作者本人,也可以扫描文章封面中的二维码订阅前端周刊微信号。

脚注:本文是在 http://lucasfcosta.com/2017/02/17/JavaScript-Errors-and-Stack-Traces.html 的基础上做了大量修改而成,英文好的同学可以直接读原文,因为考虑到最后那部分离多数工程师实际工作较远,就没有翻译。

from: http://developer.51cto.com/art/201703/534464.htm

你不知道的JavaScript错误和调用栈常识相关推荐

  1. 【译】理解Javascript函数执行—调用栈、事件循环、任务等

    原文作者:Gaurav Pandvia 原文链接:medium.com/@gaurav.pan- 文中部分链接可能需要梯子. 欢迎批评指正. 现如今,web开发者(我们更喜欢被叫做前端工程师)用一门脚 ...

  2. 「译」理解Javascript函数执行—调用栈、事件循环、任务等

    现如今,web开发者(我们更喜欢被叫做前端工程师)用一门脚本语言就能做任何事情,从提供浏览器中的交互,到开发电脑游戏.桌面工具.跨平台移动应用,甚至可以在服务端部署(如最流行的Node.js)来连结任 ...

  3. [译]深入理解JavaScript函数执行—调用栈,事件循环和任务等

    Web 开发者,或者前端工程师(我们更喜欢别人这么称呼)现如今几乎能做所有的工作,从扮演一个浏览器内部交互性的角色,到制作电脑游戏.桌面控件.跨平台手机应用,甚至还可以把它写在服务器端(最流行的是no ...

  4. JavaScript的调用栈、回调队列和事件循环

    译者按 这篇文章可以看做是对Philip Roberts 2014年在JSConf演讲的<What the heck is the event loop anyway?>的一个总结. 建议 ...

  5. JavaScript是如何工作的:引擎,运行时间以及调用栈的概述

    JavaScript是如何工作的:引擎,运行时以及调用栈的概述 原文:How JavaScript works: an overview of the engine, the runtime, and ...

  6. node.js超过php,在nodejs中如何解决超出最大的调用栈错误

    这篇文章主要介绍了nodejs超出最大的调用栈错误问题,需要的朋友可以参考下 今天早上老大和我说之前项目里面的那个数据要改动,要对 mongodb 中每条记录进行 update 操作,你写个脚本跑一下 ...

  7. 精读《你不知道的javascript》中卷

    前言 <你不知道的 javascript>是一个前端学习必读的系列,让不求甚解的JavaScript开发者迎难而上,深入语言内部,弄清楚JavaScript每一个零部件的用途.本书< ...

  8. 读书笔记-你不知道的JavaScript(上)

    本文首发在我的个人博客:http://muyunyun.cn/ <你不知道的JavaScript>系列丛书给出了很多颠覆以往对JavaScript认知的点, 读完上卷,受益匪浅,于是对其精 ...

  9. javascript错误处理与调试(转)

    JavaScript 在错误处理调试上一直是它的软肋,如果脚本出错,给出的提示经常也让人摸不着头脑. ECMAScript 第 3 版为了解决这个问题引入了 try...catch 和 throw 语 ...

最新文章

  1. java基础知识总结,绝对经典
  2. [Android]动态加载/热部署框架汇总
  3. MVC3教程之新手入门(转)
  4. python --> Python初阶 --> 基础语法 --> 条件和分支
  5. 华为云专家向宇:工欲善其事必先利其器,才能做数据的“管家”
  6. 1至100之和用c语言表达方式,C语言菜鸟基础教程之求1到100的和
  7. 云服务器Tomcat版本升级(Tomcat6升级至Tomcat7和Tomcat8)问题总结
  8. [分布式系列]Gossip协议
  9. NHibernate之旅(9):探索父子关系(一对多关系)
  10. 风云四(FY-4)气象卫星 tif文件解析成txt
  11. redis desktop manager安装以及使用教程
  12. Linux笔记本电脑大调查:程序员最喜欢的电脑是什么配置?
  13. 【Qt5】关于Qt5对xp的兼容说明
  14. 通用获取公众号文章历史,阅读量接口
  15. BigDecimal
  16. python输出图形效果的代码_python打印图形大全(详解)
  17. pdf文件过大,如何缩小的操作教程
  18. 【求职】格灵深瞳 Java 方向面经
  19. unity接入百度人体识别
  20. UBNT rocket M5 无线设置的有关笔记——Advanced Setting

热门文章

  1. 友商逼急 雷急跳墙:生死看淡 不服就干
  2. 以太坊“拿下”世界银行(WB)!7300万美元债券将在下周完成结算
  3. Google和eBay在建设微服务生态系统中的深刻教训
  4. Presto实现原理和美团的使用实践
  5. 白话Elasticsearch57-数据建模之实现悲观锁并发控制的三种方式(未成功)
  6. Spring Cloud【Finchley】-13 Eureka Server HA高可用 2个/3个节点的搭建及服务注册调用
  7. 《数据结构》知识点Day_01
  8. blp模型 上读下写_Java高并发编程(三):Java内存模型
  9. Ubuntu 安装docker-engine的三种方法
  10. 2021-02-23 Matlab数据导入--importdata和load函数