错误处理方案
有一个计算数字相加的函数,先判断传入的参数,参数不对就return结束运行
=================
抛出错误 throw
说明
抛出的类型
throw 操作符必须有一个值,但值的类型不限。
但很少抛出基本数据类型,一般都是抛出一个对象,因为对象可以包含更多的信息(比如错误码、错误提示信息)
throw 12345;
throw "Hello world!";
throw true;
// 但很少抛出基本数据类型,一般都是抛出一个对象,因为对象可以包含更多的信息
throw { errorCode:1001, errorMsg:"错误提示信息"};
但是每次写这么长的对象又有点麻烦,所以我们可以创建一个类,然后new 这个类就可以了
实际上JS已经给我们提供了这样的类,就是Error 类。
自己定义类,多用于服务器给我们返回错误信息的时候使用。
Error 错误的类型
基本说明
调用栈显示的就是,报错发生在哪些调用中的,最上面的就是报错直接发生的地方,然后就是上一层,一层一层往上。
其他错误类型
1、InternalError 底层引擎
类型的错误会在底层 JavaScript 引擎抛出异常时由浏览器抛出。
例如,递归过多导致了栈溢出。这个类型并不是代码中通常要处理的错误,如果真发生了这种错误,很可能代码哪里弄错了或者有危险了。
2、EvalError eval()函数
类型的错误会在使用 eval()函数发生异常时抛出。
基本上,只要不把 eval()当成函数调用就会报告该错误
3、RangeError 数值越界
错误会在数值越界时抛出。例如,定义数组时如果设置了并不支持的长度,如-20 或Number.MAX_VALUE
4、ReferenceError 找不到对象
会在找不到对象时发生。(这就是著名的”object expected”浏览器错误的原因。)
这种错误经常是由访问不存在的变量而导致的
let obj = x; // 在 x 没有声明时会抛出 ReferenceError
5、SyntaxError 语法错误
经常在给 eval()传入的字符串包含 JavaScript 语法错误时发生
eval("a ++ b"); // 抛出 SyntaxError
6、TypeError 类型错误
在 JavaScript 中很常见,主要发生在变量不是预期类型,或者访问不存在的方法时。
很多原因可能导致这种错误,尤其是在使用类型特定的操作而变量类型不对时。
let o = new 10; // 抛出 TypeError
console.log("name" in true); // 抛出 TypeError
Function.prototype.toString.call("name"); // 抛出 TypeError
7、URIError URI格式错误
只会在使用 encodeURI()或 decodeURI()但传入了格式错误的URI 时发生。
这个错误恐怕是 JavaScript 中难得一见的错误了,因为上面这两个函数非常稳健。
使用 throw 操作符时,代码立即停止执行,除非 try/catch 语句捕获了抛出的值。
var r, n, s;
try {
s = prompt('请输入一个数字');
n = parseInt(s);
if (isNaN(n)) {
throw new Error('输入错误');//允许抛出任意对象,包括数字、字符串。但是,最好还是抛出一个Error对象
}
// 计算平方:
r = n * n;
console.log(n + ' * ' + n + ' = ' + r);
} catch (e) {
console.log('出错了:' + e);
}
==================
异常的处理
为什么要处理?
try catch 捕获异常
格式:
捕获了异常后,程序不会终止执行,会继续执行。
处理特定类型 instanceof
try {
someFunction();
} catch (error){
if (error instanceof TypeError){
// 处理类型错误
} else if (error instanceof ReferenceError){
// 处理引用错误
} else {
// 处理所有其他类型的错误
}
}
==================
浏览器错误报告
所有主流桌面浏览器,包括 IE/Edge、Firefox、Safari、Chrome 和 Opera,都提供了向用户报告错误的机制。
默认情况下,所有浏览器都会隐藏错误信息。
一个原因是除了开发者之外这些信息对别人没什么用,另一个原因是网页在正常操作中报错的固有特性。
所有现代桌面浏览器都会通过控制台暴露错误。
这些错误可以显示在开发者工具内嵌的控制台中。
如Chrome 的按F12弹出的控制台
====================
error 事件
任何没有被 try/catch 语句处理的错误都会在 window 对象上触发 error 事件。
在 onerror 事件处理程序中,任何浏览器都不会传入 event 对象。
相反,会传入 3 个参数:错误消息、发生错误的 URL 和行号。
在任何错误发生时,无论是否是浏览器生成的,都会触发 error 事件并执行这个事件处理程序。
window.onerror = (message, url, line) => {
console.log(message);
// 可以返回 false 来阻止浏览器默认报告错误的行为
// 理想情况下,最好永远不要用到。
return false;
};
适当使用 try/catch 语句意味着不会有错误到达浏览器这个层次,因此也就不会触发 error事件。
浏览器在使用这个事件处理错误时存在明显差异。
图片错误
图片也支持 error 事件。
任何时候,如果图片 src 属性中的 URL 没有返回可识别的图片格式,就会触发 error 事件。
这个事件遵循 DOM 格式,返回一个以图片为目标的 event 对象。
const image = new Image();
image.addEventListener("load", (event) => {
console.log("图片加载成功!");
});
image.addEventListener("error", (event) => {
console.log("图片加载失败!");
});
image.src = "doesnotexist.gif"; // 不存在,资源会加载失败
当 error 事件发生时,图片下载过程已结束,不会再恢复。
====================
错误处理策略
过去,Web 应用程序的错误处理策略基本上是在服务器上落地。
错误处理策略涉及很多错误和错误处理考量,包括日志记录和监控系统。
这些主要是为了分析模式,以期找到问题的根源并了解有多少用户会受错误影响。
在 Web 应用程序的 JavaScipt 层面落地错误处理策略同样重要。
因为任何 JavaScript 错误都可能导致网页无法使用,所以理解这些错误会在什么情况下发生以及为什么会发生非常重要。
绝大多数 Web 应用程序的用户不懂技术,在碰到页面出问题时通常会迷惑。
为解决问题,他们可能会尝试刷新页面,也可能会直接放弃。
作为开发者,应该非常清楚自己的代码在什么情况下会失败,以及失败会导致什么结果。
另外,还要有一个系统跟踪这些问题。
识别错误
错误处理非常重要的部分是首先识别错误可能会在代码中的什么地方发生。
因为 JavaScript 是松散类型的,不会验证函数参数,所以很多错误只有在代码真正运行起来时才会出现。
通常,需要注意 3 类错误,误会在特定情况下,在没有对值进行充分检测时发生
1、类型转换错误
2、数据类型错误
3、通信错误
静态代码分析器
不得不说的是,通过在代码构建流程中添加静态代码分析或代码检查器(linter),可以预先发现非常多的错误。
常用的静态分析工具是 JSHint、JSLint、Google Closure 和 TypeScript。
静态代码分析器要求使用类型、函数签名及其他指令来注解 JavaScript,以此描述程序如何在基本可执行代码之外运行。
分析器会比较注解和 JavaScript 代码的各个部分,对在实际运行时可能出现的潜在不兼容问题给出提醒。
随着代码数量的增长,代码分析器会变得越来越重要,尤其是协作开发者也在增加的情况下。
所有主流技术公司都有着庞大的 JavaScript 库,并会在构建流程中使用稳健的静态分析工具。
1、类型转换错误
主要原因是使用了会自动改变某个值的数据类型的操作符或语言构造。
判断相等
使用等于(==)或不等于(!=)操作符,以及在 if、for 或 while 等流控制语句中使用非布尔值,经常会导致类型转换错误。
相等和不相等操作符会自动把执行比较的两个不同类型的值转换为相同类型。
大多数情况下,最好使用严格相等(===)和严格不相等(!==)操作符来避免类型转换。
条件
类型转换错误也会发生在流控制语句中。
比如,if 语句会自动把条件表达式转换为布尔值,然后再决定下一步的走向。在实践中,if 语句是问题比较多的。
function concat(str1, str2, str3) {
let result = str1 + str2;
if (str3) { // 不要!
result += str3;
}
return result;
}
在流控制语句中使用非布尔值作为条件是很常见的错误来源。
为避免这类错误,需要始终坚持使用布尔值作为条件。
function concat(str1, str2, str3){
let result = str1 + str2;
if (typeof str3 === "string") { // 恰当的比较
result += str3;
}
return result;
}
在这个重写的版本中,if 语句的条件会基于比较操作返回布尔值。这个函数相对更安全,受错误值影响的可能性也更小。
2、数据类型错误
因为 JavaScript 是松散类型的,所以变量和函数参数都不能保证会使用正确的数据类型。
开发者需要自己检查数据类型,确保不会发生错误。
数据类型错误常发生在将意外值传给函数的时候。
function getQueryString(url) {
if (typeof url === "string") { // 通过类型检查保证安全
let pos = url.indexOf("?");
if (pos > -1) {
return url.substring(pos +1);
}
}
return "";
}
// 还是不安全的函数,非数组值可能导致错误
function reverseSort(values) {
if (values != null){ // 不要!
values.sort();
values.reverse();
}
}
与 null 比较不足以保证适当的值,因此不要使用这种方式。出于同样的原因,也不推荐与 undefined 比较。
// 仍是不安全的函数,非数组值可能导致错误
function reverseSort(values) {
if (typeof values.sort === "function") { // 不要!
values.sort();
values.reverse();
}
}
// 安全,非数组值被忽略
function reverseSort(values) {
if (values instanceof Array) { // 修复
values.sort();
values.reverse();
}
}
检测类型
一般来说,原始类型的值应该使用 typeof 检测,而对象值应该使用 instanceof 检测。
根据函数的用法,不一定要检查每个参数的数据类型,但对外的任何 API 都应该做类型检查以保证正确执行。
3、通信错误
随着 Ajax 编程的出现,Web 应用程序在运行期间动态加载数据和功能成为常见的情形。JavaScript和服务器之间的通信也会出现错误。
第一种错误是 URL 格式或发送数据的格式不正确。通常,在把数据发送到服务器之前没有用encodeURIComponent()编码,会导致这种错误。
http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d
// 可以通过用 encodeURIComponent()编码"redir="后面的内容来修复
http://www.example.com/?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd
对于查询字符串,应该都要通过 encodeURIComponent()编码。为此,可以专门定义一个处理查询字符串的函数
// 要添加查询字符串的 URL、参数名和参数值
function addQueryStringArg(url, name, value) {
if (url.indexOf("?") == -1){
// 如果 URL 不包含问号,则要给它加上一个
url += "?";
} else {
// 否则就要使用和号(&),以便拼接更多参数和值
url += "&";
}
url += '${encodeURIComponent(name)=${encodeURIComponent(value)}';
return url;
}
const url = "http://www.somedomain.com";
const newUrl = addQueryStringArg(url, "redir",
"http://www.someotherdomain.com?a=b&c=d");
console.log(newUrl);
使用这个函数而不是手动构建 URL 可以保证编码合适,以避免相关错误发生。
在服务器响应非预期值时也会发生通信错误。
在动态加载脚本或样式时,请求的资源有可能不可用。
有些浏览器在没有返回预期资源时会静默失败,而其他浏览器则会报告错误。
不过,在动态加载资源的情况下出错,是不太好做错误处理的。
有时候,使用 Ajax 通信可能会提供关于错误条件的更多信息。
把错误记录到服务器中
Web 应用程序开发中的一个常见做法是建立中心化的错误日志存储和跟踪系统。
数据库和服务器错误正常写到日志中并按照常用 API 加以分类。
对复杂的 Web 应用程序而言,最好也把 JavaScript 错误发送回服务器记录下来。
这样做可以把错误记录到与服务器相同的系统,只要把它们归类到前端错误即可。
使用相同的系统可以进行相同的分析,而不用考虑错误来源。
要建立 JavaScript 错误日志系统,首先需要在服务器上有页面或入口可以处理错误数据。
该页面只要从查询字符串中取得错误数据,然后把它们保存到错误日志中即可。
// logError()函数接收两个参数:严重程度和错误消息。
function logError(sev, msg) {
let img = new Image(),
encodedSev = encodeURIComponent(sev),
encodedMsg = encodeURIComponent(msg);
img.src = 'log.php?sev=${encodedSev}&msg=${encodedMsg}';
}
严重程度可以是数值或字符串,具体取决于使用的日志系统。这里使用 Image 对象发送请求主要是从灵活性方面考虑的。
所有浏览器都支持 Image 对象,即使不支持 XMLHttpRequest 对象也一样。
不受跨域规则限制。
通常,接收错误消息的应该是多个服务器中的一个,而 XMLHttpRequest此时就比较麻烦。
记录错误的过程很少出错。
大多数 Ajax 通信借助 JavaScript 库的包装来处理。如果这个库本身出错,而你又要利用它记录错误,那么显然错误消息永远不会发给服务器。
for (let mod of mods){
try {
mod.init();
} catch (ex){
logError("nonfatal", 'Module init failed: ${ex.message}');
}
}
在这个例子中,模块初始化失败就会调用 logError()函数。
调试技术
JavaScript 调试器出现以前,开发者必须使用创造性的方法调试代码。
其中最为常用的调试技术是在相关代码中插入 alert(),这种方式既费事(调试完之后还得清理)又麻烦(如果有漏洞的警告框出现在产品环境中,会给用户造成不便)。
已不再推荐将警告框用于调试,因为有其他更好的解决方案。
把消息记录到控制台
所有主流浏览器都有 JavaScript 控制台,该控制台可用于查询 JavaScript 错误。
另外,这些浏览器都支持通过 console 对象直接把 JavaScript 消息写入控制台,这个对象包含如下方法。
console.error("在控制台中记录错误消息")
console.info("在控制台中记录信息性内容")
console.log("在控制台中记录常规消息")
console.error("在控制台中记录警告消息")
把消息输出到 JavaScript 控制台可以辅助调试代码,但在产品环境下应该删除所有相关代码。这可以在部署时使用代码自动完成清理,也可以手动删除。
使用 JavaScript 调试器
在运行时碰到 debugger 关键字时,所有主流浏览器都会打开开发者工具面板,并在指定位置显示断点。
function pauseExecution(){
console.log("Will print before breakpoint");
debugger;
console.log("Will not print until breakpoint continues");
}
然后,可以通过单独的浏览器控制台在断点所在的特定词法作用域中执行代码。
此外,还可以执行标准的代码调试器操作(单步进入、单步跳过、继续,等等)。
浏览器也支持在开发者工具的源代码标签页中选择希望设置断点的代码行来手动设置断点(不使用debugger 关键字)。
这样设置的断点与使用 debugger 关键字设置的一样,只是不会在不同浏览器会话之间保持。
重写console.log()
// 把所有参数拼接为一个字符串,然后打印出结果
console.log = function() {
// 'arguments'并没有 join 方法,这里先把它转换为数组
const args = Array.prototype.slice.call(arguments);
console.log(args.join(', '));
}
这样,其他代码调用的将是这个函数,而不是通用的日志方法。
这样的修改在页面刷新后会失效,因此只是调试或拦截日志的一个有用而轻量的策略。
抛出错误
抛出错误是调试代码的很好方式。
如果错误消息足够具体,只要看一眼错误就可以确定原因。
好的错误消息包含关于错误原因的确切信息,因此可以减少额外调试的工作量。
function divide(num1, num2) {
if (typeof num1 != "number" || typeof num2 != "number"){
// 任何一个参数不是数值都会抛出错误。
// 错误消息中包含函数名和错误的具体原因
throw new Error("divide(): Both arguments must be numbers.");
}
return num1 / num2;
}
当浏览器报告这个错误消息时,你立即就能根据它包含的信息定位到问题,包括问题的解决方案。
相对于没那么具体的浏览器错误消息,这个错误消息显示更有价值。
在大型应用程序中,自定义错误通常使用 assert()函数抛出错误。这个函数接收一个应该为 true的条件,并在条件为 false 时抛出错误。
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
// 这个 assert()函数可用于代替多个 if 语句,同时也是记录错误的好地方。
function divide(num1, num2) {
assert(typeof num1 == "number" && typeof num2 == "number",
"divide(): Both arguments must be numbers.");
return num1 / num2;
}
相比于之前,使用 assert()函数可以减少抛出自定义错误所需的代码量,并且让代码更好理解。