script error
script error 和 JavaScript 中的错误类型
什么是”Script error”
使用 JavaScript 的 onerror 事件处理异常, 很有可能会遇到”Script error”这样的错误.
“Script error”其实是由跨域的第三方JavaScript文件引起的. 这是个很令人头疼的错误, 因为尽管出现了错误, 但是我们无法得知具体是怎样的错误, 也不知道这个错误产生的位置. 这时, 就可以使用window.onerror
来了解错误的具体信息.
起因: 跨域脚本
查看以下HTML代码, 假设这部分代码由是这个 http://example.com/test 网页的HTML:
1 |
|
以下是 http://another-domain.com/app.js 的内容, 只声明了一个函数 foo
, 调用foo
始终会抛出ReferenceError.
1 | // another-domain.com/app.js |
浏览器加载HTML文档, 执行 JavaScript 文件之后, 控制台就会输出以下内容(通过window.onerror
回调输出)
1 | "Script error.", "", 0, 0, undefined |
这并不是 JavaScript 的 bug, 浏览器会可以隐藏跨域脚本的错误信息以确保安全性. 这样的方式能够避免脚本不小心在onerror回调中泄露敏感信息. 因此, 只有在同域的window.onerror
回调函数中, 才可以看到详细的错误信息, 除此之外, 只能看到”Script errpr”
尽管浏览器出于好意隐藏了详细错误信息, 但是有很多情况下我们需要知道跨域脚本究竟产生了怎样的错误:
- 应用的JavaScript文件位于不同域下.
- 使用托管于CDN的库, 比如cdnjs.
- 使用商业性的第三方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 | // <!-- note: crossorigin="anonymous" intentionally absent --> |
第三方JS文件:
1 | // another-domain.com/app.js |
运行上述HTML, 会输出以下内容:
1 | => ReferenceError: bar is not defined |
第一个输出的声明来自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 | var pi = 3.14159 |
ReferenceError
引用错误: 引用作用域内不存在的变量抛出的错误
1 | function foo() { |
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 | var foo = {} |
URIError
encodeURI()/decodeURI() 中传入的参数格式不是合法的URI, 会抛出此错误
decodeURIComponent("%"); // URIError
EvalError
不合理使用eval()
函数, 会抛出该错误. 但在最新版本的EcmaScript标准中, 已不再使用该错误类型.
处理异常
JavaScript 使用 try…catch…finally
处理程序中的异常:
1 | try { |
使用 try
之后必须跟上 catch
或者 finally
, 或者两者均有.
catch
语句
尽管 catch
是可选语句, 但是处理异常的代码本应是存在的. catch
语句会阻止异常在调用栈中(…), 这样即使出现异常, 程序还能正常运行. propagating through the call stack. 如果try
语句中的异常发生, 程序的控制权就转给了catch
语句中的代码. 同时所发生的异常也被传入其中. 以下例子说明了catch
语句是如何处理”ReferenceError”的, ReferenceError对象传入通过参数’exception’传入catch
1 | try { |
复杂的程序会产生各种各样的异常, 这种情况下, 可以利用instanceof
操作符来区分不同类型的异常. 以下例子中, 假定try
语句中产生了多种类型的异常, 紧随其后的catch
语句通过instanceof
操作符区分不同的异常并对其做出相应的处理.
1 | try { |
finally
语句
finally
语句中的代码始终会执行, 常在其中加入一些总结处理的代码(例如关闭文件…). 即使发生的异常没有被捕获, finally
语句也会执行, 这种情况下, finally
中的语句执行, 然后被抛出的异常正常执行. 还有一点要注意, 即使try
, catch
中有return
声明, finally
语句也会执行. 因此以下函数返回的是false
1 | function foo() { |
抛出异常
JavaScript 允许开发者抛出自定义的异常, 通过throw
声明实现. 没有经验的开发者或许会很困惑, 一般开发者都会努力写出没有任何错误的代码, 可是throw
声明却主动引入错误. 实际上, 这样的方式能够帮助开发者写出更加容易调试和维护的代码. 在自定义异常中抛出合理的错误信息使得问题更容易被发现和解决.
下面是几个throw
声明的例子. 对于声明中抛出的异常类型和抛出次数, JavaScript都没有做出限制.
1 | throw true; |
虽然throw
声明中可以包含任何数据类型, 使用内置的异常类型始终是更好的选择. 比如在Firefox浏览器下, 如果抛出的是内置异常类型, 浏览器就会在这些对象上添加一些调试信息, 例如错误所在文件名及行号.
举个例子, 你在程序中进行了除法操作, 但是被除数却是0, 这时结果就会是Infinity
(原文是NaN
, 但是现在是Infinity
, 应该是新标准). 这种情况下, 调试起来就会很困难. 如果在被除数是0的情况下抛出异常就能使调试变得容易:
1 | if (denominator === 0) |
自定义异常对象
处理自定义错误信息之外, 我们还能通过extend Error
类型自定义错误对象(继承Error
), 然后像使用其他内置异常类型一样使用自定义的错误对象.
1 | // 创建自定义对象 |