在 Web 应用程序的 JavaScript 这一端,错误处理策略也同样重要。由于任何 JavaScript 错误都可能导致网页无法使用,很多在开发过程中会忽略这个细节,因此搞清楚何时以及为什么发生错误至关重要。作为开发人员,必须要知道代码何时可能出错,会出什么错,同时还要有一个跟踪此类问题的系统。
常见的错误类型
错误处理的核心,是首先要知道代码里会发生什么错误。由于 JavaScript 是松散类型的,而且也不会验证函数的参数,因此错误只会在代码运行期间出现。一般来说,需要关注三种错误:
-
类型转换错误
-
数据类型错误
-
通信错误
以上错误分别会在特定的模式下或者没有对值进行足够的检查的情况下发生。
1. 类型转换错误
类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时。在使用相等(==)和不相等(!=)操作符,或者在 if、for 及 while 等流控制语句中使用非布尔值时,最常发生类型转换错误。
相等和不相等操作符在执行比较之前会先转换不同类型的值。由于在非动态语言中,开发人员都使用相同的符号执行直观的比较,因此在 JavaScript 中往往也会以相同方式错误地使用它们。多数情况下,我们建议使用全等(=)和不全等(!)操作符,以避免类型转换。来看一个例子。
alert(5 == "5"); //true alert(5 === "5"); //false alert(1 == true); //true alert(1 === true); //false
这里使用了相等和全等操作符比较了数值 5 和字符串"5"。相等操作符首先会将数值 5 转换成字符串"5",然后再将其与另一个字符串"5"进行比较,结果是 true。全等操作符知道要比较的是两种不同的数据类型,因而直接返回 false。对于 1 和 true 也是如此:相等操作符认为它们相等,而全等操作符认为它们不相等。使用全等和非全等操作符,可以避免发生因为使用相等和不相等操作符引发的类型转换错误,因此我们强烈推荐使用。
容易发生类型转换错误的另一个地方,就是流控制语句。像 if 之类的语句在确定下一步操作之前,会自动把任何值转换成布尔值。尤其是 if 语句,如果使用不当,最容易出错。来看下面的例子。
function concat(str1, str2, str3){ var result = str1 + str2; if (str3){ //绝对不要这样!!! result += str3; } return result; }
这个函数的用意是拼接两或三个字符串,然后返回结果。其中,第三个字符串是可选的,因此必须要检查。未使用过的命名变量会自动被赋予 undefined 值。而 undefined 值可以被转换成布尔值 false,因此这个函数中的 if 语句实际上只适用于提供了第三个参数的情况。问题在于,并不是只有 undefined 才会被转换成 false,也不是只有字符串值才可以转换为 true。例如,假设第三个参数是数值 0,那么 if 语句的测试就会失败,而对数值 1 的测试则会通过。
在流控制语句中使用非布尔值,是极为常见的一个错误来源。为避免此类错误,就要做到在条件比较时切实传入布尔值。实际上,执行某种形式的比较就可以达到这个目的。例如,我们可以将前面的函数重写如下。
function concat(str1, str2, str3){ var result = str1 + str2; if (typeof str3 == "string"){ //恰当的比较 result += str3; } return result; }
在这个重写后的函数中,if 语句的条件会基于比较返回一个布尔值。这个函数相对可靠得多,不容易受非正常值的影响。
2. 数据类型错误
JavaScript 是松散类型的,也就是说,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。为了保证不会发生数据类型错误,只能依靠开发人员编写适当的数据类型检测代码。在将预料之外的值传递给函数的情况下,最容易发生数据类型错误。
在前面的例子中,通过检测第三个参数可以确保它是一个字符串,但是并没有检测另外两个参数。如果该函数必须要返回一个字符串,那么只要给它传入两个数值,忽略第三个参数,就可以轻易地导致它的执行结果错误。类似的情况也存在于下面这个函数中。
//不安全的函数,任何非字符串值都会导致错误function getQueryString(url){ var pos = url.indexOf("?"); if (pos > -1){ return url.substring(pos +1); } return ""; }
这个函数的用意是返回给定 URL 中的查询字符串。为此,它首先使用 indexOf()
寻找字符串中的问号。如果找到了,利用 substring()
方法返回问号后面的所有字符串。这个例子中的两个函数只能操作字符串,因此只要传入其他数据类型的值就会导致错误。而添加一条简单的类型检测语句,就可以确保函数不那么容易出错
function getQueryString(url){ if (typeof url == "string"){ //通过检查类型确保安全 var pos = url.indexOf("?"); if (pos > -1){ return url.substring(pos +1); } } return ""; }
重写后的这个函数首先检查了传入的值是不是字符串。这样,就确保了函数不会因为接收到非字符串值而导致错误。
在流控制语句中使用非布尔值作为条件很容易导致类型转换错误。同样,这样做也经常会导致数据类型错误。来看下面的例子
//不安全的函数,任何非数组值都会导致错误function reverseSort(values){ if (values){ //绝对不要这样!!! values.sort(); values.reverse(); } }
这个 reverseSort()
函数可以将数组反向排序,其中用到了 sort()和 reverse()方法。对于 if语句中的控制条件而言,任何会转换为 true 的非数组值都会导致错误。另一个常见的错误就是将参数与 null 值进行比较,如下所示。
//不安全的函数,任何非数组值都会导致错误function reverseSort(values){ if (values != null){ //绝对不要这样!!! values.sort(); values.reverse(); } }
与 null 进行比较只能确保相应的值不是 null 和 undefined(这就相当于使用相等和不相等操作)。要确保传入的值有效,仅检测 null 值是不够的;因此,不应该使用这种技术。同样,我们也不推荐将某个值与 undefined 作比较
另一种错误的做法,就是只针对要使用的某一个特性执行特性检测。来看下面的例子。
//还是不安全,任何非数组值都会导致错误function reverseSort(values){ if (typeof values.sort == "function"){ //绝对不要这样!!! values.sort(); values.reverse(); } }
在这个例子中,代码首先检测了参数中是否存在 sort()方法。这样,如果传入一个包含 sort()方法的对象(而不是数组)当然也会通过检测,但在调用 reverse()函数时可能就会出错了。在确切知道应该传入什么类型的情况下,最好是使用 instanceof 来检测其数据类型,如下所示
//安全,非数组值将被忽略function reverseSort(values){ if (values instanceof Array){ //问题解决了 values.sort(); values.reverse(); } }
最后一个reverseSort()
函数是安全的:它检测了 values,以确保这个参数是 Array 类型的实例。这样一来,就可以保证函数忽略任何非数组值。
大体上来说,基本类型的值应该使用 typeof
来检测,而对象的值则应该使用 instanceof
来检测。根据使用函数的方式,有时候并不需要逐个检测所有参数的数据类型。但是,面向公众的 API 则必须无条件地执行类型检查,以确保函数始终能够正常地执行。
3. 通信错误
随着 Ajax 编程的兴起,Web 应用程序在其生命周期内动态加载信息或功能,已经成为一件司空见惯的事。不过,JavaScript 与服务器之间的任何一次通信,都有可能会产生错误。
第一种通信错误与格式不正确的 URL 或发送的数据有关。最常见的问题是在将数据发送给服务器之前,没有使用 encodeURIComponent()
对数据进行编码。例如,下面这个 URL 的格式就是不正确的:
针对"redir="后面的所有字符串调用 encodeURIComponent()
就可以解决这个问题,结果将产生如下字符串:
对于查询字符串,应该记住必须要使用 encodeURIComponent()
方法。为了确保这一点,有时候可以定义一个处理查询字符串的函数,例如:
function addQueryStringArg(url, name, value){ if (url.indexOf("?") == -1){ url += "?"; } else { url += "&"; } url += encodeURIComponent(name) + "=" + encodeURIComponent(value); return url; }
这个函数接收三个参数:要追加查询字符串的 URL、参数名和参数值。如果传入的 URL 不包含问号,还要给它添加问号;否则,就要添加一个和号,因为有问号就意味着有其他查询字符串。然后,再将经过编码的查询字符串的名和值添加到 URL 后面。可以像下面这样使用这个函数:
var url = "http://www.somedomain.com"; var newUrl = addQueryStringArg(url, "redir", "http://www.someotherdomain.com?a=b&c=d"); alert(newUrl);
使用这个函数而不是手工构建 URL,可以确保编码正确并避免相关错误。
另外,在服务器响应的数据不正确时,也会发生通信错误。动态加载脚本和动态加载样式,运用这两种技术都有可能遇到资源不可用的情况。在没有返回相应资源的情况下,Firefox、Chrome 和 Safari 会默默地失败,IE 和 Opera 则都会报错。然而,对于使用这两种技术产生的错误,很难判断和处理。在某些情况下,使用 Ajax 通信可以提供有关错误状态的更多信息。