script error 和 JavaScript 中的错误类型

什么是”Script error”

使用 JavaScript 的 onerror 事件处理异常, 很有可能会遇到”Script error”这样的错误.

“Script error”其实是由跨域的第三方JavaScript文件引起的. 这是个很令人头疼的错误, 因为尽管出现了错误, 但是我们无法得知具体是怎样的错误, 也不知道这个错误产生的位置. 这时, 就可以使用window.onerror来了解错误的具体信息.

起因: 跨域脚本

查看以下HTML代码, 假设这部分代码由是这个 http://example.com/test 网页的HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html>
<head>
<title>example.com/test</title>
</head>
<body>
<script src="http://another-domain.com/app.js"></script>
<script>
window.onerror = function (message, url, line, column, error) {
console.log(message, url, line, column, error);
}
foo(); // call function declared in app.js
</script>
</body>
</html>

以下是 http://another-domain.com/app.js 的内容, 只声明了一个函数 foo, 调用foo始终会抛出ReferenceError.

1
2
3
4
// another-domain.com/app.js
function foo() {
bar(); // ReferenceError: bar is not a function
}

浏览器加载HTML文档, 执行 JavaScript 文件之后, 控制台就会输出以下内容(通过window.onerror回调输出)

1
"Script error.", "", 0, 0, undefined

这并不是 JavaScript 的 bug, 浏览器会可以隐藏跨域脚本的错误信息以确保安全性. 这样的方式能够避免脚本不小心在onerror回调中泄露敏感信息. 因此, 只有在同域的window.onerror回调函数中, 才可以看到详细的错误信息, 除此之外, 只能看到”Script errpr”

尽管浏览器出于好意隐藏了详细错误信息, 但是有很多情况下我们需要知道跨域脚本究竟产生了怎样的错误:

  1. 应用的JavaScript文件位于不同域下.
  2. 使用托管于CDN的库, 比如cdnjs.
  3. 使用商业性的第三方JavaScript库, 只存在于外部服务器.

为了解决上述的问题, 必须要知道错误的详细信息, 我们只需要实行以下操作:

解决办法: CORS属性及头部

1.添加crossorigin=”anonymous”到 script 标签的属性中:

1
<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>

以上属性告诉浏览器匿名获取目标文件, 这样的话, 浏览器请求该文件时, 就不会发送潜在的泄露用户资料的信息, 如cookie或HTTP认证信息.

2.添加支持跨域的HTTP头部

1
Access-Control-Allow-Origin: *

CORS指: 跨域资源共享, 由一系列的API组成(其中大部分是HTTP头部), 这些API用以说明跨域文件应该以怎样的方式被获取.

通过设置Access-Control-Allow-Origin: *. 服务器给浏览器这样的指示: 任何域名下的文件都可以获取该文件. 同样可以将*换成特定的域名:

1
Access-Control-Allow-Origin: https://www.example.com

通过以上两步的配置, 任何由该文件引起的错误详细信息都会通过window.onerror的回调函数抛出, 不再是”Script error”

1
"ReferenceError: bar is not defined", "http://another-domain.com/app.js", 2, 1, [Object Error]

另一种解决方式: 使用 try/catch

有时候, 我们没有权限修改所使用的第三方JS头部. 这样的情况下, 就得使用try/catch方法, 见下例, 不添加crossorigin="anonymous"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// <!-- note: crossorigin="anonymous" intentionally absent -->
<script src="http://another-domain.com/app.js"></script>
<script>
window.onerror = function (message, url, line, column, error) {
console.log(message, url, line, column, error);
}

try {
foo(); // call function declared in app.js
} catch (e) {
console.log(e);
throw e; // intentionally re-throw (caught by window.onerror)
}
</script>

第三方JS文件:

1
2
3
4
// another-domain.com/app.js
function foo() {
bar(); // ReferenceError: bar is not a function
}

运行上述HTML, 会输出以下内容:

1
2
3
4
5
=> ReferenceError: bar is not defined
at foo (http://another-domain.com/b.js:2:3)
at http://example.com/test/:15:3

=> "Script error.", "", 0, 0, undefined

第一个输出的声明来自try/catch的捕获, 获得了错误对象, 含有错误类型, 错误信息以及堆栈轨迹, 包括文件名以及错误所在行号. 第二个输出的声明来自window.onerror的捕获, 只能够输出”Script error”, 没有其它有效信息.

尽管try/catch是一种解决办法, 但是能够修改HTML文档和CORS头部的情况下, 用window.onerror是更好的方式.

JavaScript中的异常处理

JS代码抛出异常后, JS解释器(interpreter)会执行处理异常的代码, 如果没有处理异常的代码, 应用就会在抛出异常的函数中 return. 一步步执行执行栈中的函数, 直到找到处理异常的函数或到达执行栈顶部的函数, 应用内的终止运行.

Error 对象

当异常发生, Error 对象会被创建并抛出, JavaScript 定义了7种类型的错误对象:

Error

一般开发者自定义的异常会使用该对象, 使用以下方式定义:

var error = new Error("error message")

Error对象有两个属性, name 和 message. name 指异常的类型, 此时为 Error. message 是对该错误的具体描述, 以参数形式传入, 此时是”error message”. 其他6种类型的更加具体Error对象也含有同样的这两种属性.

RangeError

试图传递一个number参数给一个范围内不包含该number的函数时会引发RangeError. 当传递一个不合法的length值作为Array 构造器的参数创建数组, 或者传递错误值到数值计算方法(Number.toExponential(), Number.toFixed(), Number.toPrecision())

1
2
var pi = 3.14159
pi.toFixed(10000) // RangeError

ReferenceError

引用错误: 引用作用域内不存在的变量抛出的错误

1
2
3
function foo() {
bar++; // ReferenceError
}

SyntaxError

语法错误: C 和 Java在编译过程中, 会抛出 Syntax Error, 但 JavaScript 作为解释性语言, 代码执行过程中才会发现Syntax Error. Syntax errors are unique as they are the only type of exception that cannot be recovered from.

1
if (foo) {  // SyntaxError

TypeError

当某个值并非预期的类型, 会抛出 TypeError, 这种情况常出现在调用不存在的对象方法时

1
2
3
var foo = {}

foo.bar() // TypeError

URIError

encodeURI()/decodeURI() 中传入的参数格式不是合法的URI, 会抛出此错误

decodeURIComponent("%"); // URIError

EvalError

不合理使用eval()函数, 会抛出该错误. 但在最新版本的EcmaScript标准中, 已不再使用该错误类型.

处理异常

JavaScript 使用 try…catch…finally 处理程序中的异常:

1
2
3
4
5
6
7
try {
// 意图执行, 有可能抛出异常的代码
} catch (exception) {
// 处理意图执行的代码抛出的异常
} finally {
// 始终会执行的代码
}

使用 try 之后必须跟上 catch 或者 finally, 或者两者均有.

catch语句

尽管 catch是可选语句, 但是处理异常的代码本应是存在的. catch语句会阻止异常在调用栈中(…), 这样即使出现异常, 程序还能正常运行. propagating through the call stack. 如果try语句中的异常发生, 程序的控制权就转给了catch语句中的代码. 同时所发生的异常也被传入其中. 以下例子说明了catch语句是如何处理”ReferenceError”的, ReferenceError对象传入通过参数’exception’传入catch

1
2
3
4
5
6
try {
foo++; // ReferenceError
} catch (exception) {
var message = exception.message;
// 处理异常
}

复杂的程序会产生各种各样的异常, 这种情况下, 可以利用instanceof操作符来区分不同类型的异常. 以下例子中, 假定try语句中产生了多种类型的异常, 紧随其后的catch语句通过instanceof操作符区分不同的异常并对其做出相应的处理.

1
2
3
4
5
6
7
8
9
10
11
try {
// 假定此处发生异常
} catch (exception) {
if (exception instanceof TypeError) {
// 处理 TypeError 异常
} else if (exception instanceof ReferenceError) {
// 处理 TypeError 异常
} else {
// 处理其他类型的异常
}
}

finally语句

finally语句中的代码始终会执行, 常在其中加入一些总结处理的代码(例如关闭文件…). 即使发生的异常没有被捕获, finally语句也会执行, 这种情况下, finally中的语句执行, 然后被抛出的异常正常执行. 还有一点要注意, 即使try, catch中有return声明, finally语句也会执行. 因此以下函数返回的是false

1
2
3
4
5
6
7
function foo() {
try {
return true;
} finally {
return false;
}
}

抛出异常

JavaScript 允许开发者抛出自定义的异常, 通过throw声明实现. 没有经验的开发者或许会很困惑, 一般开发者都会努力写出没有任何错误的代码, 可是throw声明却主动引入错误. 实际上, 这样的方式能够帮助开发者写出更加容易调试和维护的代码. 在自定义异常中抛出合理的错误信息使得问题更容易被发现和解决.

下面是几个throw声明的例子. 对于声明中抛出的异常类型和抛出次数, JavaScript都没有做出限制.

1
2
3
4
5
6
7
throw true;
throw 5;
throw "error message";
throw null;
throw undefined;
throw {};
throw new SyntaxError("useful error message");

虽然throw声明中可以包含任何数据类型, 使用内置的异常类型始终是更好的选择. 比如在Firefox浏览器下, 如果抛出的是内置异常类型, 浏览器就会在这些对象上添加一些调试信息, 例如错误所在文件名及行号.

举个例子, 你在程序中进行了除法操作, 但是被除数却是0, 这时结果就会是Infinity(原文是NaN, 但是现在是Infinity, 应该是新标准). 这种情况下, 调试起来就会很困难. 如果在被除数是0的情况下抛出异常就能使调试变得容易:

1
2
3
4
5
6
if (denominator === 0)
throw new Error("Attempted division by zero!");
// 更加合适的错误类型: RangeError:

if (denominator === 0)
throw new RangeError("Attempted division by zero!");

自定义异常对象

处理自定义错误信息之外, 我们还能通过extend Error类型自定义错误对象(继承Error), 然后像使用其他内置异常类型一样使用自定义的错误对象.

1
2
3
4
5
6
7
8
9
// 创建自定义对象
function DivisionByZeroError(message) {
this.name = "DivisionByZeroError";
this.message = (message || "");
}

// 实现继承
DivisionByZeroError.prototype = new Error();
DivisionByZeroError.prototype.constructor = DivisionByZeroError;

Resources