17.错误处理与调试(1)

错误处理与调试:由于JavaScript 本身是动态语言,而且多年来一直没有固定的开发工具,因此人们普遍认为它是一种最难于调试的编程语言。脚本出错时,浏览器通常会给出类似于“object expected”(缺少对象)这样的消息,没有上下文信息,让人摸不着头脑。ECMAScript 第3 版致力于解决这个问题,专门引入了try-catch 和throw 语句以及一些错误类型,意在让开发人员能够适当地处理错误。几年之后,Web 浏览器中也出现了一些JavaScript 调试程序和工具。2008 年以来,大多数Web 浏览器都已经具备了一些调试JavaScript 代码的能力。

在有了语言特性和工具支持之后,现在的开发人员已经能够适当地实现错误处理,并且能够找到错误的根源。

1.浏览器报告的错误:IE、Firefox、Safari、Chrome 和Opera 等主流浏览器,都具有某种向用户报告JavaScript 错误的机制。默认情况下,所有浏览器都会隐藏此类信息,毕竟除了开发人员之外,很少有人关心这些内容。因此,在基于浏览器编写JavaScript 脚本时,别忘了启用浏览器的JavaScript 报告功能,以便及时收到错误通知。

  • IE:IE 是唯一一个在浏览器的界面窗体(chrome)中显示JavaScript 错误信息的浏览器。在发生JavaScript错误时,浏览器左下角会出现一个黄色的图标,图标旁边则显示着"Error on page"(页面中有错误)。假如不是存心去看的话,你很可能不会注意这个图标。双击这个图标,就会看到一个包含错误消息的对话框,其中还包含诸如行号、字符数、错误代码及文件名(其实就是你在查看的页面的URL)等相关信息。

这些信息对于一般用户还算说得过去,但对Web 开发来说就远远不够了。可以通过设置让错误对话框一发生错误就显示出来。为此,要打开“Tools”(工具)菜单中的“Internet Options”(Internet 选项)对话框,切换到“Advanced”(高级)选项卡,选中“Display a notification about every script error”(显示每个脚本错误的通知)复选框。单击“OK”(确定)按钮保存设置。

保存了设置之后,通常要双击黄色图标才会显示的对话框,就会变成一有错误发生随即自动显示出来。

另外,如果启用了脚本调试功能的话(默认是禁用的),那么在发生错误时,你不仅会显示错误通知,而且还会看到另一
个对话框,询问是否想要调试错误。

要启用脚本调试功能,必须要在IE 中安装某种脚本调试器。(IE8 和IE9 自带调试器。)本章后面会单独讨论调试器。

在IE7 及更早版本中,如果错误发生在位于外部文件的脚本中,行号通常会与错误所在的行号差1。如果是嵌入在页面中的脚本发生错误,则行号就是错误所在的行号。

  • Firefox:默认情况下,Firefox 在JavaScript 发生错误时不会通过浏览器界面给出提示。但它会在后台将错误记录到错误控制台中。单击“Tools”(工具)菜单中的“Error Console”(错误控制台)可以显示错误控制台。你会发现,错误控制台中实际上还包含与JavaScript、CSS 和HTML 相关的警告和信息,可以通过筛选找到错误。

在发生JavaScript 错误时,Firefox 会将其记录为一个错误,包括错误消息、引发错误的URL 和错误所在的行号等信息。单击文件名即可以只读方式打开发生错误的脚本,发生错误的代码行会突出显示。

目前,最流行的Firefox 插件Firebug,已经成为开发人员必备的JavaScript 纠错工具。这个可以从www.getfirebug.com 下载到的插件,会在Firefox 状态栏的右下角区域添加一个图标。默认情况下,右下角区域显示的是一个绿色对勾图标。在有JavaScript 错误发生时,图标会变成红叉,同时旁边显示错误的数量。单击这个红叉会打开Firebug 控制台,其中显示有错误消息、错误所在的代码行(不包含上下文)、错误所在的URL 以及行号。

在Firebug 中单击导致错误的代码行,将在一个新Firebug 视图中打开整个脚本,该代码行在其中突出显示。

除了显示错误之外,Firebug 还有更多的用处。实际上,它还是针对Firefox 的成熟的调试环境,为调试JavaScript、CSS、DOM 和网络连接错误提供了诸多功能。

  • Safari:Windows 和Mac OS 平台的Safari 在默认情况下都会隐藏全部JavaScript 错误。为了访问到这些信息,必须启用“Develop”(开发)菜单。为此,需要单击“Edit”(编辑)菜单中的“Preferences”(偏好设置),然后在“Advanced”(高级)选项卡中,选中“Show develop menu in menubar”(在菜单栏中显示“开发”菜单)。启用此项设置之后,就会在Safari 的菜单栏中看到一个“Develop”菜单。

“Develop”菜单中提供了一些与调试有关的选项,还有一些选项可以影响当前加载的页面。单击“Show Error Console”(显示错误控制台)选项,将会看到一组JavaScript 及其他错误。控制台中显示着错误消息、错误的URL 及错误的行号。

单击控制台中的错误消息,就可以打开导致错误的源代码。除了被输出到控制台之外,JavaScript错误不会影响Safari 窗口的外观。

  • Opera:Opera 在默认情况下也会隐藏JavaScript 错误,所有错误都会被记录到错误控制台中。要打开错误控制台,需要单击“Tools”(工具)菜单,在“Advanced”(高级)子菜单项下面再单击“Error Console”(错误控制台)。与Firefox 一样,Opera 的错误控制台中也包含了除JavaScript 错误之外的很多来源(如HTML、CSS、XML、XSLT 等)的错误和警告信息。要分类查看不同来源的消息,可以使用左下角的下拉选择框。

错误消息中显示着导致错误的URL 和错误所在的线程。有时候,还会有栈跟踪信息。除了错误控制台中显示的信息之外,没有其他途径可以获得更多信息。

也可以让Opera 一发生错误就弹出错误控制台。为此,要在“Tools”(工具)菜单中单击“Preferences”(首选项),再单击“Advanced”(高级)选项卡,然后从左侧菜单中选择“Content”(内容)。单击“JavaScripOptions”(JavaScript 选项)按钮,显示选项对话框。

在这个选项对话框中,选中“Open console on error”(出错时打开控制台),单击“OK”(确定)按钮。这样,每当发生JavaScript错误时,就会弹出错误控制台。另外,还可以针对特定的站点来作此设置,方法是单击“Tools”(工具)、“Quick Preferences”(快速参数)、“Edit Site Preferences”(编辑站点首选项),选择“Scripting”(脚本)选项卡,最后选中“Open console on error”(出错时打开控制台)。

  • Chrome:与Safari 和Opera 一样,Chrome 在默认情况下也会隐藏JavaScript 错误。所有错误都将被记录到Web Inspector 控制台中。要查看错误消息,必须打开Web Inspector。为此,要单击位于地址栏右侧的“Control this page”(控制当前页)按钮,选择“Developer”(开发人员)、“JavaScript console”(JavaScript控制台)。

打开的Web Inspector 中包含着有关页面的信息和JavaScript 控制台。控制台中显示着错误消息、错误的URL 和错误的行号。

单击JavaScript 控制台中的错误,就可以定位到导致错误的源代码行。

2.错误处理:错误处理在程序设计中的重要性是勿庸置疑的。任何有影响力的Web 应用程序都需要一套完善的错误处理机制,当然,大多数佼佼者确实做到了这一点,但通常只有服务器端应用程序才能做到如此。实际上,服务器端团队往往会在错误处理机制上投入较大的精力,通常要考虑按照类型、频率,或者其他重要的标准对错误进行分类。这样一来,开发人员就能够理解用户在使用简单数据库查询或者报告生成脚本时,应用程序可能会出现的问题。

虽然客户端应用程序的错误处理也同样重要,但真正受到重视,还是最近几年的事。实际上,我们要面对这样一个不争的事实:使用Web 的绝大多数人都不是技术高手,其中甚至有很多人根本就不明白浏览器到底是什么,更不用说让他们说喜欢哪一个了。本章前面讨论过,每个浏览器在发生JavaScript错误时的行为都或多或少有一些差异。有的会显示小图标,有的则什么动静也没有,浏览器对JavaScript错误的这些默认行为对最终用户而言,毫无规律可循。最理想的情况下,用户遇到错误搞不清为什么,他们会再试着重做一次;最糟糕的情况下,用户会恼羞成怒,一去不复返了。良好的错误处理机制可以让用户及时得到提醒,知道到底发生了什么事,因而不会惊惶失措。为此,作为开发人员,我们必须理解在处理JavaScript 错误的时候,都有哪些手段和工具可以利用。

  • try-catch语句:ECMA-262 第3 版引入了try-catch 语句,作为JavaScript 中处理异常的一种标准方式。基本的语法如下所示,显而易见,这与Java 中的try-catch 语句是完全相同的。
try{
    // 可能会导致错误的代码
} catch(error){
    // 在错误发生时怎么处理
}

也就是说,我们应该把所有可能会抛出错误的代码都放在try 语句块中,而把那些用于错误处理的代码放在catch 块中。例如:

try {
    window.someNonexistentFunction();
} catch (error){
    alert("An error happened!");
}

如果try 块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行catch 块。此时,catch 块会接收到一个包含错误信息的对象。与在其他语言中不同的是,即使你不想使用这个错误对象,也要给它起个名字。这个对象中包含的实际信息会因浏览器而异,但共同的是有一个保存着错误消息的message 属性。ECMA-262 还规定了一个保存错误类型的name 属性;当前所有浏览器都支持这个属性(Opera 9 之前的版本不支持这个属性)。因此,在发生错误时,就可以像下面这样实事求是地显示浏览器给出的消息。

try {
    window.someNonexistentFunction();
} catch (error){
    alert(error.message);
}

这个例子在向用户显示错误消息时,使用了错误对象的message 属性。这个message 属性是唯一一个能够保证所有浏览器都支持的属性,除此之外,IE、Firefox、Safari、Chrome 以及Opera 都为事件对象添加了其他相关信息。IE 添加了与message 属性完全相同的description 属性,还添加了保存着内部错误数量的number 属性。Firefox 添加了fileName、lineNumber 和stack(包含栈跟踪信息)属性。Safari 添加了line(表示行号)、sourceId(表示内部错误代码)和sourceURL 属性。当然,在跨浏览器编程时,最好还是只使用message 属性。

1. finally 子句:虽然在try-catch 语句中是可选的,但finally 子句一经使用,其代码无论如何都会执行。换句话说,try 语句块中的代码全部正常执行,finally 子句会执行;如果因为出错而执行了catch 语句块,finally 子句照样还会执行。只要代码中包含finally子句,则无论try 或catch 语句块中包含什么代码——甚至return 语句,都不会阻止finally 子句的执行。来看下面这个函数。

function testFinally(){
    try {
        return 2;
    } catch (error){
        return 1;
    } finally {
        return 0;
    }
}

这个函数在try-catch 语句的每一部分都放了一条return 语句。表面上看,调用这个函数会返回2,因为返回2 的return 语句位于try 语句块中,而执行该语句又不会出错。可是,由于最后还有一个finally 子句,结果就会导致该return 语句被忽略;也就是说,调用这个函数只能返回0。如果把finally 子句拿掉,这个函数将返回2。

如果提供finally 子句,则catch 子句就成了可选的(catch 或finally 有一个即可)。IE7 及更早版本中有一个bug:除非有catch 子句,否则finally 中的代码永远不会执行。如果你仍然要考虑IE 的早期版本,那就只好提供一个catch 子句,哪怕里面什么都不写。IE8 修复了这个bug。

只要代码中包含finally 子句,那么无论try 还是catch 语句块中的return 语句都将被忽略。因此,在使用finally 子句之前,一定要非常清楚你想让代码怎么样。

2. 错误类型:执行代码期间可能会发生的错误有多种类型。每种错误都有对应的错误类型,而当错误发生时,就会抛出相应类型的错误对象。ECMA-262 定义了下列7 种错误类型:

  • Error:其中,Error 是基类型,其他错误类型都继承自该类型。因此,所有错误类型共享了一组相同的属性(错误对象中的方法全是默认的对象方法)。Error 类型的错误很少见,如果有也是浏览器抛出的;这个基类型的主要目的是供开发人员抛出自定义错误。
  • EvalError:EvalError 类型的错误会在使用eval()函数而发生异常时被抛出。ECMA-262 中对这个错误有如下描述:“如果以非直接调用的方式使用eval 属性的值(换句话说,没有明确地将其名称作为一个Identifier,即用作CallExpression 中的MemberExpression),或者为eval 属性赋值。”简单地说,如果没有把eval()当成函数调用,就会抛出错误,例如:
new eval(); //抛出EvalError
eval = foo; //抛出EvalError

在实践中,浏览器不一定会在应该抛出错误时就抛出EvalError。例如,Firefox 4+和IE8 对第一种情况会抛出TypeError,而第二种情况会成功执行,不发生错误。有鉴于此,加上在实际开发中极少会这样使用eval(),所以遇到这种错误类型的可能性极小。

  • RangeError:RangeError 类型的错误会在数值超出相应范围时触发。例如,在定义数组时,如果指定了数组不支持的项数(如-20 或Number.MAX_VALUE),就会触发这种错误。下面是具体的例子。
var items1 = new Array(-20); //抛出RangeError
var items2 = new Array(Number.MAX_VALUE); //抛出RangeError
  • ReferenceError:JavaScript 中经常会出现这种范围错误。在找不到对象的情况下,会发生ReferenceError(这种情况下,会直接导致人所共知的"objectexpected"浏览器错误)。通常,在访问不存在的变量时,就会发生这种错误,例如:
var obj = x; //在x 并未声明的情况下抛出 ReferenceError
  • SyntaxError:至于SyntaxError,当我们把语法错误的JavaScript 字符串传入eval()函数时,就会导致此类错误。例如:
eval("a ++ b"); //抛出SyntaxError

如果语法错误的代码出现在eval()函数之外,则不太可能使用SyntaxError,因为此时的语法错误会导致JavaScript 代码立即停止执行。

  • TypeError:TypeError 类型在JavaScript 中会经常用到,在变量中保存着意外的类型时,或者在访问不存在的方法时,都会导致这种错误。错误的原因虽然多种多样,但归根结底还是由于在执行特定于类型的操作时,变量的类型并不符合要求所致。下面来看几个例子。
var o = new 10; //抛出TypeError
alert("name" in true); //抛出TypeError
Function.prototype.toString.call("name"); //抛出TypeError

最常发生类型错误的情况,就是传递给函数的参数事先未经检查,结果传入类型与预期类型不相符。

  • URIError:在使用encodeURI()或decodeURI(),而URI 格式不正确时,就会导致URIError 错误。这种错误也很少见,因为前面说的这两个函数的容错性非常高。

利用不同的错误类型,可以获悉更多有关异常的信息,从而有助于对错误作出恰当的处理。要想知道错误的类型,可以像下面这样在try-catch 语句的catch 语句中使用instanceof 操作符。

try {
    someFunction();
} catch (error){
    if (error instanceof TypeError){
        //处理类型错误
    } else if (error instanceof ReferenceError){
        //处理引用错误
    } else {
        //处理其他类型的错误
    }
}

在跨浏览器编程中,检查错误类型是确定处理方式的最简便途径;包含在message 属性中的错误消息会因浏览器而异。

3. 合理使用try-catch:当try-catch 语句中发生错误时,浏览器会认为错误已经被处理了,因而不会通过本章前面讨论的机制记录或报告错误。对于那些不要求用户懂技术,也不需要用户理解错误的Web 应用程序,这应该说是个理想的结果。不过,try-catch 能够让我们实现自己的错误处理机制。

使用try-catch 最适合处理那些我们无法控制的错误。假设你在使用一个大型JavaScript 库中的函数,该函数可能会有意无意地抛出一些错误。由于我们不能修改这个库的源代码,所以大可将对该函数的调用放在try-catch 语句当中,万一有什么错误发生,也好恰当地处理它们。

在明明白白地知道自己的代码会发生错误时,再使用try-catch 语句就不太合适了。例如,如果传递给函数的参数是字符串而非数值,就会造成函数出错,那么就应该先检查参数的类型,然后再决定如何去做。在这种情况下,不应用使用try-catch 语句。

  • 抛出错误:与try-catch 语句相配的还有一个throw 操作符,用于随时抛出自定义错误。抛出错误时,必须要给throw 操作符指定一个值,这个值是什么类型,没有要求。下列代码都是有效的。
throw 12345;
throw "Hello world!";
throw true;
throw { name: "JavaScript"};

在遇到throw 操作符时,代码会立即停止执行。仅当有try-catch 语句捕获到被抛出的值时,代码才会继续执行。

通过使用某种内置错误类型,可以更真实地模拟浏览器错误。每种错误类型的构造函数接收一个参数,即实际的错误消息。下面是一个例子。

throw new Error("Something bad happened.");

这行代码抛出了一个通用错误,带有一条自定义错误消息。浏览器会像处理自己生成的错误一样,来处理这行代码抛出的错误。换句话说,浏览器会以常规方式报告这一错误,并且会显示这里的自定义错误消息。像下面使用其他错误类型,也可以模拟出类似的浏览器错误。

throw new SyntaxError("I don’t like your syntax.");
throw new TypeError("What type of variable do you take me for?");
throw new RangeError("Sorry, you just don’t have the range.");
throw new EvalError("That doesn’t evaluate.");
throw new URIError("Uri, is that you?");
throw new ReferenceError("You didn’t cite your references properly.");

在创建自定义错误消息时最常用的错误类型是Error、RangeError、ReferenceError 和TypeError。

另外,利用原型链还可以通过继承Error 来创建自定义错误类型(原型链在第6 章中介绍)。此时,需要为新创建的错误类型指定name 和message 属性。来看一个例子。

function CustomError(message){
    this.name = "CustomError";
    this.message = message;
}
CustomError.prototype = new Error();
throw new CustomError("My message");

浏览器对待继承自Error 的自定义错误类型,就像对待其他错误类型一样。如果要捕获自己抛出的错误并且把它与浏览器错误区别对待的话,创建自定义错误是很有用的。

IE 只有在抛出Error 对象的时候才会显示自定义错误消息。对于其他类型,它都无一例外地显示"exception thrown and not caught"(抛出了异常,且未被捕获)。

1. 抛出错误的时机:要针对函数为什么会执行失败给出更多信息,抛出自定义错误是一种很方便的方式。应该在出现某种特定的已知错误条件,导致函数无法正常执行时抛出错误。换句话说,浏览器会在某种特定的条件下执行函数时抛出错误。例如,下面的函数会在参数不是数组的情况下失败。

function process(values){
    values.sort();
    for (var i=0, len=values.length; i < len; i++){
        if (values[i] > 100){
            return values[i];
        }
    }
    return -1;
}

如果执行这个函数时传给它一个字符串参数,那么对sort()的调用就会失败。对此,不同浏览器会给出不同的错误消息,但都不是特别明确,如下所示。

IE:属性或方法不存在。
Firefox:values.sort()不是函数。
Safari:值undefined(表达式values.sort 的结果)不是对象。
Chrome:对象名没有方法'sort'。
Opera:类型不匹配(通常是在需要对象的地方使用了非对象值)。

尽管Firefox、Chrome 和Safari 都明确指出了代码中导致错误的部分,但错误消息并没有清楚地告诉我们到底出了什么问题,该怎么修复问题。在处理类似前面例子中的那个函数时,通过调试处理这些错误消息没有什么困难。但是,在面对包含数千行JavaScript 代码的复杂的Web 应用程序时,要想查找错误来源就没有那么容易了。这种情况下,带有适当信息的自定义错误能够显著提升代码的可维护性。来看下面的例子。

function process(values){
    if (!(values instanceof Array)){
        throw new Error("process(): Argument must be an array.");
    }
    values.sort();
    for (var i=0, len=values.length; i < len; i++){
        if (values[i] > 100){
            return values[i];
        }
    }
    return -1;
}

在重写后的这个函数中,如果values 参数不是数组,就会抛出一个错误。错误消息中包含了函数的名称,以及为什么会发生错误的明确描述。如果一个复杂的Web 应用程序发生了这个错误,那么查找问题的根源也就容易多了。

建议读者在开发JavaScript 代码的过程中,重点关注函数和可能导致函数执行失败的因素。良好的错误处理机制应该可以确保代码中只发生你自己抛出的错误。

2. 抛出错误与使用try-catch:关于何时该抛出错误,而何时该使用try-catch 来捕获它们,是一个老生常谈的问题。一般来说,应用程序架构的较低层次中经常会抛出错误,但这个层次并不会影响当前执行的代码,因而错误通常得不到真正的处理。如果你打算编写一个要在很多应用程序中使用的JavaScript 库,甚至只编写一个可能会在应用程序内部多个地方使用的辅助函数,我都强烈建议你在抛出错误时提供详尽的信息。然后,即可在应用程序中捕获并适当地处理这些错误。

说到抛出错误与捕获错误,我们认为只应该捕获那些你确切地知道该如何处理的错误。捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息。

  • 错误(error)事件:任何没有通过try-catch 处理的错误都会触发window 对象的error 事件。这个事件是Web 浏览器最早支持的事件之一,IE、Firefox 和Chrome 为保持向后兼容,并没有对这个事件作任何修改(Opera和Safari 不支持error 事件)。在任何Web 浏览器中,onerror 事件处理程序都不会创建event 对象,但它可以接收三个参数:错误消息、错误所在的URL 和行号。多数情况下,只有错误消息有用,因为URL 只是给出了文档的位置,而行号所指的代码行既可能出自嵌入的JavaScript 代码,也可能出自外部的文件。要指定onerror 事件处理程序,必须使用如下所示的DOM0 级技术,它没有遵循“DOM2 级事件”的标准格式。
window.onerror = function(message, url, line){
    alert(message);
};

只要发生错误,无论是不是浏览器生成的,都会触发error 事件,并执行这个事件处理程序。然后,浏览器默认的机制发挥作用,像往常一样显示出错误消息。像下面这样在事件处理程序中返回false,可以阻止浏览器报告错误的默认行为。

window.onerror = function(message, url, line){
    alert(message);
    return false;
};

通过返回false,这个函数实际上就充当了整个文档中的try-catch 语句,可以捕获所有无代码处理的运行时错误。这个事件处理程序是避免浏览器报告错误的最后一道防线,理想情况下,只要可能就不应该使用它。只要能够适当地使用try-catch 语句,就不会有错误交给浏览器,也就不会触发error 事件。

浏览器在使用这个事件处理错误时的方式有明显不同。在IE 中,即使发生error事件,代码仍然会正常执行;所有变量和数据都将得到保留,因此能在onerror 事件处理程序中访问它们。但在Firefox 中,常规代码会停止执行,事件发生之前的所有变量和数据都将被销毁,因此几乎就无法判断错误了。

图像也支持error 事件。只要图像的src 特性中的URL 不能返回可以被识别的图像格式,就会触发error 事件。此时的error 事件遵循DOM格式,会返回一个以图像为目标的event 对象。下面是一个例子。

var image = new Image();
EventUtil.addHandler(image, "load", function(event){
    alert("Image loaded!");
});
EventUtil.addHandler(image, "error", function(event){
    alert("Image not loaded!");
});
image.src = "smilex.gif"; //指定不存在的文件

在这个例子中,当加载图像失败时就会显示一个警告框。需要注意的是,发生error 事件时,图像下载过程已经结束,也就是说不能再重新下载了。

  • 处理错误的策略:过去,所谓Web 应用程序的错误处理策略仅限于服务器端。在谈到错误与错误处理时,通常要考虑很多方面,涉及一些工具,例如记录和监控系统。这些工具的用途在于分析错误模式,追查错误原因,同时帮助确定错误会影响到多少用户。

在Web 应用程序的JavaScript 这一端,错误处理策略也同样重要。由于任何JavaScript 错误都可能导致网页无法使用,因此搞清楚何时以及为什么发生错误至关重要。绝大多数Web 应用程序的用户都不懂技术,遇到错误时很容易心烦意乱。有时候,他们可能会刷新页面以期解决问题,而有时候则会放弃努力。作为开发人员,必须要知道代码何时可能出错,会出什么错,同时还要有一个跟踪此类问题的系统。

  • 常见的错误类型:错误处理的核心,是首先要知道代码里会发生什么错误。由于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 编程的兴起(第21 章讨论Ajax),Web 应用程序在其生命周期内动态加载信息或功能,已经成为一件司空见惯的事。不过,JavaScript 与服务器之间的任何一次通信,都有可能会产生错误。

第一种通信错误与格式不正确的URL 或发送的数据有关。最常见的问题是在将数据发送给服务器之前,没有使用encodeURIComponent()对数据进行编码。例如,下面这个URL 的格式就是不正确的:

http://www.yourdomain.com/?redir=http://www.someotherdomain.com?a=b&c=d

针对"redir="后面的所有字符串调用encodeURIComponent()就可以解决这个问题,结果将产生如下字符串:

http://www.yourdomain.com/?redir=http%3A%2F%2Fwww.someotherdomain.com%3Fa%3Db%26c%3Dd

对于查询字符串,应该记住必须要使用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,可以确保编码正确并避免相关错误。

另外,在服务器响应的数据不正确时,也会发生通信错误。第10 章曾经讨论过动态加载脚本和动态加载样式,运用这两种技术都有可能遇到资源不可用的情况。在没有返回相应资源的情况下,Firefox、Chrome 和Safari 会默默地失败,IE 和Opera 则都会报错。然而,对于使用这两种技术产生的错误,很难判断和处理。在某些情况下,使用Ajax 通信可以提供有关错误状态的更多信息。

在使用Ajax 通信的情况下,也可能会发生通信错误。

  • 区分致命错误和非致命错误:任何错误处理策略中最重要的一个部分,就是确定错误是否致命。对于非致命错误,可以根据下列一或多个条件来确定:
  1. 不影响用户的主要任务;
  2. 只影响页面的一部分;
  3. 可以恢复;
  4. 重复相同操作可以消除错误。

本质上,非致命错误并不是需要关注的问题。例如,Yahoo! Mail(http://mail.yahoo.com)有一项功能,允许用户在其界面上发送手机短信。如果由于某种原因,发不了手机短信了,那也不算是致命错误,因为并不是应用程序的主要功能有问题。用户使用Yahoo! Mail 主要是为了查收和撰写电子邮件。只在这个主要功能正常,就没有理由打断用户。没有必要因为发生了非致命错误而对用户给出提示——可以把页面中受到影响的区域替换掉,比如替换成说明相应功能无法使用的消息。但是,如果因此打断用户,那确实没有必要。

致命错误,可以通过以下一或多个条件来确定:

  1. 应用程序根本无法继续运行;
  2. 错误明显影响到了用户的主要操作;
  3. 会导致其他连带错误。

要想采取适当的措施,必须要知道JavaScript 在什么情况下会发生致命错误。在发生致命错误时,应该立即给用户发送一条消息,告诉他们无法再继续手头的事情了。假如必须刷新页面才能让应用程序正常运行,就必须通知用户,同时给用户提供一个点击即可刷新页面的按钮。

区分非致命错误和致命错误的主要依据,就是看它们对用户的影响。设计良好的代码,可以做到应用程序某一部分发生错误不会不必要地影响另一个实际上毫不相干的部分。例如,My Yahoo!(http://my.yahoo.com)的个性化主页上包含了很多互不依赖的模块。如果每个模块都需要通过JavaScript调用来初始化,那么你可能会看到类似下面这样的代码:

for (var i=0, len=mods.length; i < len; i++){
    mods[i].init(); //可能会导致致命错误
}

表面上看,这些代码没什么问题:依次对每个模块调用init()方法。问题在于,任何模块的init()方法如果出错,都会导致数组中后续的所有模块无法再进行初始化。从逻辑上说,这样编写代码没有什么意义。毕竟,每个模块相互之间没有依赖关系,各自实现不同功能。可能会导致致命错误的原因是代码的结构。不过,经过下面这样修改,就可以把所有模块的错误变成非致命的:

for (var i=0, len=mods.length; i < len; i++){
    try {
        mods[i].init();
    } catch (ex) {
        //在这里处理错误
    }
}

通过在for 循环中添加try-catch 语句,任何模块初始化时出错,都不会影响其他模块的初始化。在以上重写的代码中,如果有错误发生,相应的错误将会得到独立的处理,并不会影响到用户的体验。

  • 把错误记录到服务器:开发Web 应用程序过程中的一种常见的做法,就是集中保存错误日志,以便查找重要错误的原因。例如数据库和服务器错误都会定期写入日志,而且会按照常用API 进行分类。在复杂的Web 应用程序中,我们同样推荐你把JavaScript 错误也回写到服务器。换句话说,也要将这些错误写入到保存服务器端错误的地方,只不过要标明它们来自前端。把前后端的错误集中起来,能够极大地方便对数据的分析。

要建立这样一种JavaScript 错误记录系统,首先需要在服务器上创建一个页面(或者一个服务器入口点),用于处理错误数据。这个页面的作用无非就是从查询字符串中取得数据,然后再将数据写入错误日志中。这个页面可能会使用如下所示的函数:

function logError(sev, msg){
    var img = new Image();
    img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" +
    encodeURIComponent(msg);
}

这个logError()函数接收两个参数:表示严重程度的数值或字符串(视所用系统而异)及错误消息。其中,使用了Image 对象来发送请求,这样做非常灵活,主要表现如下几方面。

  1. 所有浏览器都支持Image 对象,包括那些不支持XMLHttpRequest 对象的浏览器。
  2. 可以避免跨域限制。通常都是一台服务器要负责处理多台服务器的错误,而这种情况下使用XMLHttpRequest 是不行的。
  3. 在记录错误的过程中出问题的概率比较低。大多数Ajax 通信都是由JavaScript 库提供的包装函数来处理的,如果库代码本身有问题,而你还在依赖该库记录错误,可想而知,错误消息是不可能得到记录的。

只要是使用try-catch 语句,就应该把相应错误记录到日志中。来看下面的例子。

for (var i=0, len=mods.length; i < len; i++){
    try {
        mods[i].init();
    } catch (ex){
        logError("nonfatal", "Module init failed: " + ex.message);
    }
}

在这里,一旦模块初始化失败,就会调用logError()。第一个参数是"nonfatal"(非致命),表示错误的严重程度。第二个参数是上下文信息加上真正的JavaScript 错误消息。记录到服务器中的错误消息应该尽可能多地带有上下文信息,以便鉴别导致错误的真正原因。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值