热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

你不知道的JavaScript错误和调用栈常识

大多数工程师可能并没留意过JS中错误对象、错误堆栈的细节,即使他们每天的日常工作会面临不少的报错,部分同学甚至在console的错误面前一脸懵逼

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

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

image

调用栈的工作机制

在探讨 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 执行完,也会被从调用栈移除。

image

为了更好的说明调用栈的工作机制,我们对上面的代码稍作改动,使用 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 // <-- 从这行往下的内容可以忽略&#xff0c;因为这些都是 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)

显而易见&#xff0c;当我们在 c 内部调用 console.trace 的时候&#xff0c;调用栈从下往上的结构是&#xff1a;a -> b -> c。如果把代码再稍作改动&#xff0c;在 b 中 c 执行完之后调用&#xff0c;如下&#xff1a;

function c() {console.log(&#39;c&#39;);
}
function b() {console.log(&#39;b&#39;);c();console.trace();
}
function a() {console.log(&#39;a&#39;);b();
}
a();

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

Traceat b (repl:4:9)at a (repl:3:1)at repl:1:1 // <-- 从这行往下的内容可以忽略&#xff0c;因为这些都是 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)

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

image

Error 对象及错误处理

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


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

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

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

image

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


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

try 语句还可以嵌套在 try 语句中&#xff0c;比如&#xff1a;

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

try 语句也可以嵌套在 catch 和 finally 语句中&#xff0c;比如下面的两个例子&#xff1a;

try {throw new Error(&#39;First error&#39;);
} catch (err) {console.log(&#39;First catch running&#39;);try {throw new Error(&#39;Second error&#39;);} catch (nestedErr) {console.log(&#39;Second catch running.&#39;);}
}
try {console.log(&#39;The try block is running...&#39;);
} finally {try {throw new Error(&#39;Error inside finally.&#39;);} catch (err) {console.log(&#39;Caught an error inside the finally block.&#39;);}
}

同样需要注意的是&#xff0c;你可以抛出不是 Error 对象的任意值。这可能看起来很酷&#xff0c;但在工程上却是强烈不建议的做法。如果恰巧你需要处理错误的调用栈信息和其他有意义的元数据&#xff0c;抛出非 Error 对象的错误会让你的处境很尴尬。

image

假如我们有如下的代码&#xff1a;

function runWithoutThrowing(func) {try {func();} catch (e) {console.log(&#39;There was an error, but I will not throw it.&#39;);console.log(&#39;The error\&#39;s message was: &#39; &#43; e.message)}
}
function funcThatThrowsError() {throw new TypeError(&#39;I am a TypeError.&#39;);
}
runWithoutThrowing(funcThatThrowsError);

如果 runWithoutThrowing 的调用者传入的函数都能抛出 Error 对象&#xff0c;这段代码不会有任何问题&#xff0c;如果他们抛出了字符串那就有问题了&#xff0c;比如&#xff1a;

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

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

image

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

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

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

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

image

错误堆栈的裁剪

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

让我们用代码来说明&#xff0c;首先&#xff0c;把当前的调用栈捕获并放到 myObj 上&#xff1a;

const myObj &#61; {};
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&#xff0c;因为我们在 b 调用 c 之前就捕获了调用栈。现在对上面的代码稍作修改&#xff0c;然后看看会发生什么&#xff1a;

const myObj &#61; {};
function d() {// 我们把当前调用栈存储到 myObj 上&#xff0c;但是会去掉 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 &#96;b&#96; 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)

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

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

总结

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

image
最后&#xff0c;给大家推荐一个前端学习进阶内推交流群685910553&#xff08;前端资料分享&#xff09;&#xff0c;不管你在地球哪个方位&#xff0c;
不管你参加工作几年都欢迎你的入驻&#xff01;&#xff08;群内会定期免费提供一些群主收藏的免费学习书籍资料以及整理好的面试题和答案文档&#xff01;&#xff09;

如果您对这个文章有任何异议&#xff0c;那么请在文章评论处写上你的评论。

如果您觉得这个文章有意思&#xff0c;那么请分享并转发&#xff0c;或者也可以关注一下表示您对我们文章的认可与鼓励。

愿大家都能在编程这条路&#xff0c;越走越远。


推荐阅读
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • React基础篇一 - JSX语法扩展与使用
    本文介绍了React基础篇一中的JSX语法扩展与使用。JSX是一种JavaScript的语法扩展,用于描述React中的用户界面。文章详细介绍了在JSX中使用表达式的方法,并给出了一个示例代码。最后,提到了JSX在编译后会被转化为普通的JavaScript对象。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • ASP.NET2.0数据教程之十四:使用FormView的模板
    本文介绍了在ASP.NET 2.0中使用FormView控件来实现自定义的显示外观,与GridView和DetailsView不同,FormView使用模板来呈现,可以实现不规则的外观呈现。同时还介绍了TemplateField的用法和FormView与DetailsView的区别。 ... [详细]
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • JavaScript和HTML之间的交互是经由过程事宜完成的。事宜:文档或浏览器窗口中发作的一些特定的交互霎时。能够运用侦听器(或处置惩罚递次来预订事宜),以便事宜发作时实行相应的 ... [详细]
  • 本文介绍了在满足特定条件时如何在输入字段中使用默认值的方法和相应的代码。当输入字段填充100或更多的金额时,使用50作为默认值;当输入字段填充有-20或更多(负数)时,使用-10作为默认值。文章还提供了相关的JavaScript和Jquery代码,用于动态地根据条件使用默认值。 ... [详细]
  • Jquery 跨域问题
    为什么80%的码农都做不了架构师?JQuery1.2后getJSON方法支持跨域读取json数据,原理是利用一个叫做jsonp的概念。当然 ... [详细]
  • 本文总结了在编写JS代码时,不同浏览器间的兼容性差异,并提供了相应的解决方法。其中包括阻止默认事件的代码示例和猎取兄弟节点的函数。这些方法可以帮助开发者在不同浏览器上实现一致的功能。 ... [详细]
  • 本文讨论了将HashRouter改为Router后,页面全部变为空白页且没有报错的问题。作者提到了在实际部署中需要在服务端进行配置以避免刷新404的问题,并分享了route/index.js中hash模式的配置。文章还提到了在vueJs项目中遇到过类似的问题。 ... [详细]
  • 本文整理了常用的CSS属性及用法,包括背景属性、边框属性、尺寸属性、可伸缩框属性、字体属性和文本属性等,方便开发者查阅和使用。 ... [详细]
  • Vue基础一、什么是Vue1.1概念Vue(读音vjuː,类似于view)是一套用于构建用户界面的渐进式JavaScript框架,与其它大型框架不 ... [详细]
  • 引号快捷键_首选项和设置——自定义快捷键
    3.3自定义快捷键(CustomizingHotkeys)ChemDraw快捷键由一个XML文件定义,我们可以根据自己的需要, ... [详细]
author-avatar
手机用户2502937923
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有