本章内容
理解浏览器错误报告
处理错误
调试 JavaScript 代码
1 浏览器错误报告
1.1 桌面控制台
要直接进入控制台,不同操作系统和浏览器支持不同的快捷键。
1.2 移动控制台
移动浏览器不会直接在设备上提供控制台界面。Chrome 移动版和 Safari 的 iOS 版内置了实用工具,支持将设备连接到宿主操作系统中相同的浏览器。此外也可以使用第三方工具直接在移动设备上调试。Firefox 常用的调试工具是 Firebug Lite,这需要通过 JavaScript 的书签小工具向当前页面中加入 Firebug 脚本才可以。脚本运行后,就可以直接在移动浏览器上打开调试界面。Firebug Lite 也有面向其他浏览器(如 Chrome)的版本。
2 错误处理
2.1 try/catch 语句
任何可能出错的代码都应该放到 try 块中,而处理错误的代码则放在 catch 块中,如下所示:
try {
window.someNonexistentFunction();
} catch (error){
console.log(error.message);
}
1. finally 子句
如果 try 块中的代码运行完,则接着执行finally 块中的代码。如果出错并执行 catch 块中的代码,则 finally 块中的代码仍执行。try 或catch 块无法阻止 finally 块执行,包括 return 语句。比如:
function testFinally(){
try {
return 2;
} catch (error){
return 1;
} finally {
return 0;
}
}
2. 错误类型
代码执行过程中会发生各种类型的错误。每种类型都会对应一个错误发生时抛出的错误对象。ECMA-262 定义了以下 8 种错误类型:
Error
InternalError: 例如,递归过多导致了栈溢出。
EvalError: 在使用 eval()函数发生异常时抛出。基本上,只要不把 eval()当成函数调用就会报告该错误:
new eval(); // 抛出 EvalError
eval = foo; // 抛出 EvalError
RangeError: 数值越界时抛出。例如,定义数组时如果设置了并不支持的长度,就会报告该错误。
ReferenceError: 会在找不到对象时发生。
SyntaxError:r经常在给 eval()传入的字符串包含 JavaScript 语法错误时发生,eval(“a ++ b”); // 抛出 SyntaxError
TypeError:主要发生在变量不是预期类型,或者访问不存在的方法时。
URIError:只会在使用 encodeURI()或 decodeURI()但传入了格式错误的URI 时发生。
Error 是基类型,其他错误类型继承该类型。因此,所有错误类型都共享相同的属性(所有错误对象上的方法都是这个默认类型定义的方法)。浏览器很少会抛出 Error 类型的错误,该类型主要用于开发者抛出自定义错误。
try {
someFunction();
} catch (error){
if (error instanceof TypeError){
// 处理类型错误
} else if (error instanceof ReferenceError){
// 处理引用错误
} else {
// 处理所有其他类型的错误
}
}
3. try/catch 的用法
如果你明确知道自己的代码会发生某种错误,那么就不适合使用 try/catch 语句。例如,如果给函数传入字符串而不是数值时就会失败,就应该检查该函数的参数类型并采取相应的操作。这种情况下,没有必要使用 try/catch 语句。
2.2 抛出错误
与 try/catch 语句对应的一个机制是 throw 操作符,用于在任何时候抛出自定义错误。throw 操作符必须有一个值,但值的类型不限。下面这些代码都是有效的:
throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript" };
此外,通过继承 Error(第 6 章介绍过继承)也可以创建自定义的错误类型。创建自定义错误类型时,需要提供 name 属性和 message 属性,比如:
class CustomError extends Error {
constructor(message) {
super(message);
this.name = "CustomError";
this.message = message;
}
}
throw new CustomError("My message");
1. 何时抛出错误
在出现已知函数无法正确执行的情况时就应该抛出错误。
function process(values){
if (!(values instanceof Array)){
throw new Error("process(): Argument must be an array.");
}
values.sort();
for (let value of values){
if (value > 100){
return value;
}
}
return -1;
}
2. 抛出错误与 try/catch
捕获错误的目的是阻止浏览器以其默认方式响应;抛出错误的目的是为错误提供有关其发生原因的说明。
2.3 error 事件
window.onerror = (message, url, line) => {
console.log(message);
};
这个事件处理程序应该是处理浏览器报告错误的最后一道防线。理想情况下,最好永远不要用到。适当使用 try/catch 语句意味着不会有错误到达浏览器这个层次,因此也就不会触发 error事件。
注意 浏览器在使用这个事件处理错误时存在明显差异。
2.4 错误处理策略
在 Web 应用程序的 JavaScipt 层面落地错误处理策略同样重要。因为任何 JavaScript 错误都可能导致网页无法使用,所以理解这些错误会在什么情况下发生以及为什么会发生非常重要。
2.5 识别错误
类型转换错误
数据类型错误
通信错误
1. 静态代码分析器
通过在代码构建流程中添加静态代码分析或代码检查器(linter),可以预先发现非常多的错误。这样的代码分析工具有很多,详见 GitHub Gist 网站 All Gists 页面。常用的静态分析工具是 JSHint、JSLint、Google Closure 和 TypeScript。
2. 类型转换错误
类型转换错误的主要原因是使用了会自动改变某个值的数据类型的操作符或语言构造。使用等于(==)或不等于(!=)操作符,以及在 if、for 或 while 等流控制语句中使用非布尔值,经常会导致类型转换错误。使用严格相等和严格不相等操作符可以避免比较过程的类型转换错误,强烈推荐用它们代替相等和不相等操作符。
3. 数据类型错误
因为 JavaScript 是松散类型的,所以变量和函数参数都不能保证会使用正确的数据类型。开发者需要自己检查数据类型,确保不会发生错误。数据类型错误常发生在将意外值传给函数的时候。
4. 通信错误
随着 Ajax 编程的出现,Web 应用程序在运行期间动态加载数据和功能成为常见的情形。JavaScript和服务器之间的通信也会出现错误。一种错误是 URL 格式或发送数据的格式不正确。
2.6 区分重大与非重大错误
任何错误处理策略中一个非常重要的方面就是确定某个错误是否为重大错误。具有以下一个或多个特性的错误属于非重大错误:
不会影响用户的主要任务;
只会影响页面中某个部分;
可以恢复;
重复操作可能成功。
2.7 把错误记录到服务器中
对复杂的 Web 应用程序而言,最好也把 JavaScript 错误发送回服务器记录下来。这样做可以把错误记录到与服务器相同的系统,只要把它们归类到前端错误即可。使用相同的系统可以进行相同的分析,而不用考虑错误来源。
要建立 JavaScript 错误日志系统,首先需要在服务器上有页面或入口可以处理错误数据。该页面只要从查询字符串中取得错误数据,然后把它们保存到错误日志中即可。比如,该页面可以使用如下代码:
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 库的包装来处理。如果这个库本身出错,而你又要利用它记录错误,那么显然错误消息永远不会发给服务器。
只要是使用 try/catch 语句的地方,都可以把相关错误记录下来。下面是一个例子:
for (let mod of mods){
try {
mod.init();
} catch (ex){
logError("nonfatal", 'Module init failed: ${ex.message}');
}
}
3 调试技术
3.1 把消息记录到控制台
所有主流浏览器都有 JavaScript 控制台,该控制台可用于查询 JavaScript 错误。另外,这些浏览器都支持通过 console 对象直接把 JavaScript 消息写入控制台。
3.2 理解控制台运行时
浏览器控制台是个读取-求值-打印-循环(REPL,read-eval-print-loop),与页面的 JavaScript 运行时并发。在开发者工具的 Element(元素)标签页内,单击 DOM 树中一个节点,就可以在 Console(控制台)标签页中使用$0 引用该节点的 JavaScript实例。
3.3 使用 JavaScript 调试器
debugger;在运行时碰到这个关键字时,所有主流浏览器都会打开开发者工具面板,并在指定位置显示断点。然后,可以通过单独的浏览器控制台在断点所在的特定词法作用域中执行代码。此外,还可以执行标准的代码调试器操作(单步进入、单步跳过、继续,等等)。
4 旧版 IE 的常见错误
4.1 无效字符
JavaScript 文件中的代码必须由特定字符构成。在检测到 JavaScript 文件中存在无效字符时,IE 会抛出"invalid character"错误。
4.2 未找到成员
旧版 IE 中所有 DOM 对象都是用 COM 对象实现的,并非原生 JavaScript 对象。在涉及垃圾回收时,这可能会导致很多奇怪的行为。其中,"member not found"错误是 IE 中垃圾回收程序常报告的错误。
这个错误通常会在给一个已被销毁的对象赋值时发生。这个对象必须是 COM 对象才会出现这个消息。最好的一个例子就是 event 对象。IE 的 event 对象是作为 window 的一个属性存在的,会在事件发生时创建,在事件处理程序执行完毕后销毁。因此,如果你想在稍后会执行的闭包中使用 event 对象,尝试给 event 对象赋值就会导致这个错误
document.onclick = function() {
var event = window.event;
setTimeout(function(){
event.returnValue = false; // 未找到成员
}, 1000);
};
当 onclick事件处理程序退出后,event 对象会被销毁,因此闭包中对它的引用也就不存在了。
4.3 未知运行时错误
使用 innerHTML 或 outerHTML 属性以下面一种方式添加 HTML 时会发生未知运行时错误:比如将块级元素插入行内元素,或者在表格的任何部分(table、tbody等)访问了其中一个属性。例如,从技术角度来说,p标签不能包含另一个块级元素,如div,因此以下代码会导致未知运行时错误,在将块级元素插入不恰当的位置时,其他浏览器会尝试纠正,这样就不会发生错误,但 IE 在这种情况下要严格得多。
4.4 语法错误
如果网页中引用的一个外部 JavaScript 文件由于某种原因返回了非 JavaScript 代码,则 IE 会抛出语法错误。例如,错误地把
4.5 系统找不到指定资源
“The system cannot locate the resource specified”(系统找不到指定资源)。这个错误会在 JavaScript向某个 URL发送请求,而该 URL长度超过了 IE允许的最大 URL长度(2083个字符)时发生。这个长度限制不仅针对 JavaScript,而且针对 IE 本身。
5 小结
下列方法可用于阻止浏览器对 JavaScript 错误作出反应。
使用 try/catch 语句,可以通过更合适的方式对错误做出处理,避免浏览器处理。
定义 window.onerror 事件处理程序,所有没有通过 try/catch 处理的错误都会被该事件处理程序接收到(仅限 IE、Firefox 和 Chrome)。
开发 Web 应用程序时,应该认真考虑可能发生的错误,以及如何处理这些错误。
首先,应该分清哪些算重大错误,哪些不算重大错误。
然后,要通过分析代码预测很可能发生哪些错误。由于以下因素,JavaScript 中经常出现错误:
类型转换;
数据类型检测不足;
向服务器发送错误数据或从服务器接收到错误数据。