JavaScript 网页编程从入门到精通 (清华社"视频大讲堂"大系·网络开发视频大讲堂)
上QQ阅读APP看书,第一时间看更新

2.7 JavaScript代码调试

在JavaScript开发初期,开发人员常在要调试的代码中随处插入alert()函数。但这种做法一方面比较麻烦(调试之后还需要清理),另一方面还可能引入新问题,如果把alert()函数遗留在产品代码后会带来很多麻烦。如今,已经有了很多更好的调试工具,因此就不再建议在调试中使用alert()了。

2.7.1 使用控制台

IE、Firefox、Opera、Chrome和Safari都有JavaScript控制台,可以用来查看JavaScript错误。而且,在这些浏览器中都可以通过代码向控制台输出消息。对Firefox而言,需要安装Firebug(http://getfirebug.com/)因为Firefox要使用Firebug的控制台。对IE、Firefox、Chrome和Safari来说,都可以通过console对象向JavaScript控制台中写入消息,该对象包含下列方法。

error(message):将错误消息记录到控制台。

info(message):将信息性消息记录到控制台。

log(message):将一般消息记录到控制台。

warn(message):将警告消息记录到控制台。

在IE、Firebug、Chrome和Safari中,用来记录消息的方法不同,控制台中显示的错误消息也不一样。错误消息带有红色图标,而警告消息带有黄色图标。

【示例1】以下函数展示了使用控制台输出消息的一个示例。

      function sum(num1, num2){
          console.log("参数值分别为:" + num1 + "、" + num2);
          console.log("计算参数的和为:");
          var result = num1 + num2;
          console.log(result);
          return result;
      }
      sum(24, 45);

在浏览器中执行上面代码,则可以在控制台中看到输出信息,如图2-11所示。

图2-11 在IE控制台中写入信息

【示例2】不存在一种跨浏览器向JavaScript控制台写入消息的机制,但下面的函数可以作为统一的接口。

      function log(message){
          if (typeof console == 'object'){
            console.log(message);
          }else if(typeof opera == 'object'){
            opera.postError(message);
          }else if(typeof java == 'object' && typeof java.lang == 'object'){
            java.lang.System.out.println(message);
          }
      }

这个log()函数检测了哪个JavaScript控制台接口可用,然后使用相应的接口。可以在任何浏览器中安全地使用这个函数,不会导致任何错误,例如:

      function sum(num1, num2){
          log("参数值分别为:" + num1 + "、" + num2);
          log("计算参数的和为:");
          var result = num1 + num2;
          log(result);
          return result;
      }
      sum(24, 45);

向JavaScript控制台中写入消息可以辅助调试代码,但在发布应用程序时,还必须要移除所有消息。在部署应用程序时,可以通过手工或特定的代码处理步骤来自动完成清理工作。

提示:记录消息要比使用alert()函数更可取,因为警告框会阻断程序的执行,而在测定异步处理对时间的影响时,使用警告框会影响结果。

2.7.2 显示错误信息

另一种输出调试消息的方式,就是在页面中开辟一小块区域,用以显示消息。这个区域常是一个元素,而该元素可以总是出现在页面中,但仅用于调试目的,也可以是一个根据需要动态创建的元素。

【示例】针对2.7.1节的log()函数,可以将log()函数修改为如下所示:

      function log(message){
          var console = document.getElementById("debuginfo");
          if (console === null){
            console = document.createElement("div");
            console.id = "debuginfo";
            console.style.background = "#dedede";
            console.style.border = "1px solid silver";
            console.style.padding = "5px";
            console.style.width = "400px";
            console.style.position = "absolute";
            console.style.right = "0px";
            console.style.top = "0px";
            document.body.appendChild(console);
          }
          console.innerHTML += "<p>" + message + "</p>";
      }

这个修改后的log()函数首先检测是否已经存在调试元素,如果没有则会新创建一个<div>标签,并为该标签应用一些样式,以便与页面中的其他元素区别开。然后使用innerHTML将消息写入到这个<div>标签中,结果就是页面中会有一小块区域显示错误消息。这种技术在不支持JavaScript控制台的IE 7及更早版本或其他浏览器中十分有用。

然后,在页面中进行测试,则效果如图2-12所示。

图2-12 在IE中显示信息

      function sum(num1, num2){
          log("参数值分别为:" + num1 + "、" + num2);
          log("计算参数的和为:");
          var result = num1 + num2;
          log(result);
          return result;
      }
      window.onload =function(){
          sum(24, 45);
      }

提示:与把错误消息记录到控制台相似,把错误消息输出到页面的代码也要在发布前清除。

2.7.3 抛出错误

如前所述,抛出错误也是一种调试代码的好办法。如果错误消息很具体,基本上就可以把它当作确定错误来源的依据。但这种错误消息必须能够明确给出导致错误的原因,才能省去其他调试操作。

【示例】看下面的函数。

      function divide(num1, num2){
          return num1 / num2;
      }

这个简单的函数计算两个数的除法,但如果有一个参数不是数值,它会返回NaN。类似这样简单的计算如果返回NaN,就会在Web应用程序中导致问题。对此,可以在计算之前,先检测每个参数是否都是数值。

      function divide(num1, num2){
          if(typeof num1 ! = "number" || typeof num2 ! = "number")
                throw new Error("divide(): 两个参数必须是数字.");
          return num1 / num2;
      }

在此,如果有一个参数不是数值,就会抛出错误。错误消息中包含了函数的名字,以及导致错误的真正原因。浏览器只要报告了这个错误消息,我们就可以立即知道错误来源及问题的性质。相对来说,这种具体的错误消息要比那些泛泛的浏览器错误消息更有用。

对于大型应用程序来说,自定义的错误通常都使用assert()函数抛出。这个函数接收两个参数,一个是求值结果应该为true的条件,另一个是条件为false时要抛出的错误。以下就是一个非常基本的assert()函数。

      function assert(condition, message){
          if (! condition){
            throw new Error(message);
          }
      }

可以用这个assert()函数代替某些函数中需要调试的if语句,以便输出错误消息。下面是使用这个函数的例子。

      function divide(num1, num2){
          assert(typeof num1 == "number" && typeof num2 == "number",
                "divide():两个参数必须是数字.");
          return num1 / num2;
      }
      var result = divide(10, 23);
      result = divide("hi", 3);

可见,使用assert()函数可以减少抛出错误所需的代码量,而且也比前面的代码更容易看懂。

2.7.4 IE错误

多年以来,IE一直都是最难于调试JavaScript错误的浏览器。IE给出的错误消息一般很短又语意不详,而且上下文信息也很少,有时甚至一点都没有。但作为用户最多的浏览器,如何看懂给出的错误也是最受关注的。下面简单介绍在IE中常见的难于调试的JavaScript错误。

1.操作终止

在IE 8之前的版本中,存在一个相对于其他浏览器而言,最难于调试的错误:操作终止(operation aborted)。在修改尚未加载完成的页面时,就会发生操作终止错误。发生错误时,会出现一个模态对话框,提示“操作终止。”,单击“确定”(OK)按钮,则卸载整个页面,继而显示一张空白屏幕,此时要进行调试非常困难。

【示例1】下面的示例试图在页面未完全加载时就对文档结构进行操作,会引发错误。

      <!DOCTYPE html>
      <html>
      <head>
      <title></title>
      </head>
      <body>
      <div>
      <script type="text/javascript">
          document.body.appendChild(document.createElement("div"));
      </script>
      </div>
      </body>
      </html>

在这个例子中存在的问题:JavaScript代码在页面尚未加载完毕时就要修改document.body,而且<script>元素还不是<body>元素的直接子元素。准确一点说,当<script>节点被包含在某个元素中,而且JavaScript代码又要用appendChild()、innerHTML,或其他DOM方法修改该元素的父元素或祖先元素时,将会发生操作终止错误,因为它只能修改已经加载完毕的元素。

要避免这个问题,可以等到目标元素加载完毕后再对它进行操作,或者使用其他操作方法。例如,为document.body添加一个绝对定位在页面上的覆盖层,就是一种非常常见的操作。通常,开发人员都是使用appendChild()方法来添加这个元素的,但使用insertBefore()方法也很容易。因此,只要修改前面例子中的一行代码,就可以避免操作终止错误。

      <!DOCTYPE html>
      <html>
      <head>
      <title></title>
      </head>
      <body>
      <div>
      <script type="text/javascript">
          document.body.insertBefore(document.createElement("div"), document.body.firstChild);
      </script>
      </div>
      </body>
      </html>

在这个例子中,新的<div>元素被添加到document.body的开头部分而不是末尾。因为完成这一操作所需的所有信息在脚本运行时都是已知的,所以这不会引发错误。

【示例2】除了改变方法之外,还可以把<script>元素从包含元素中移来,直接作为<body>的子元素。

      <!DOCTYPE html>
      <html>
      <head>
      <title></title>
      </head>
      <body>
      <div>
      </div>
      <script type="text/javascript">
          document.body.appendChild(document.createElement("div"));
      </script>
      </body>
      </html>

这一次也不会发生错误,因为脚本修改的是它的直接父元素,而不再是间接的祖先元素。

在同样的情况下,IE 8不再抛出操作终止错误,而是抛出常规的JavaScript错误。不过,虽然浏览器抛出的错误不同,但解决方案仍然是一样的。

2.无效字符

根据语法,JavaScript文件必须只包含特定的字符。在JavaScript文件中存在无效字符时,IE会抛出无效字符(Invalid Character)错误。所谓无效字符,就是JavaScript语法中未定义的字符。

例如,有一个很像减号但却由Unicode值8211表示的字符(\u2013),就不能用作常规的减号(ASCII编码为45),因为JavaScript语法中没有定义该字符。这个字符通常是在Word文档中自动插入的。如果JavaScript代码是从Word文档中复制到文本编辑器中,然后又在IE中运行的,那么就可能会遇到无效字符错误。其他浏览器对无效字符做出的反应与IE类似,Firefox会抛出非法字符(Illegal Character)错误,Safari会报告发生了语法错误,而Opera则会报告发生了ReferenceError(引用错误),因为它会将无效字符解释为未定义的标识符。

3.未找到成员

IE中所有DOM对象都是COM对象,而非原生JavaScript对象的形式实现的。这会导致一些与垃圾收集相关的非常奇怪的行为。IE中的未找到成员(Member not found)错误,就是由于垃圾收集例程配合错误所直接导致的。

具体来说,如果在对象被销毁之后,又给该对象赋值,将会导致未找到成员错误。而导致这个错误的一定是COM对象。发生这个错误的最常见情形是使用event对象时。IE中的event对象是window的属性,该对象在事件发生时创建,在最后一个事件处理程序执行完毕后销毁。

【示例3】假设在一个闭包中使用了event对象,而该闭包不会立即执行,那么在将来调用它并给event的属性赋值时,就会导致未找到成员错误。

      document.onclick = function(){
          var event = window.event;
          setTimeout(function(){
            event.returnValue=false;          //未找到成员错误
          },100)
       }

在这段代码中,将一个单击事件处理程序指定给了文档。在事件处理程序中.window.event被保存在event变量中。然后,传入setTimeout()中的闭包里又包含了event变量。当单击事件处理程序执行完毕后,event对象就会被销毁,因而闭包中引用对象的成员就成了不存在的。因此,在闭包中给returnValue赋值就会导致未找到成员错误。

4.未知运行时错误

当使用innerHTML或outerHTML以下列方式指定HTML时,就会发生未知运行时错误(Unknown runtime error):一是把块元素插入到行内元素时,二是访问表格任意部分(<table>、<tbody>等)的任意属性时。

【示例4】从技术角度说,<span>标签不能包含<div>之类的块级元素,因此下面的代码就会导致未知运行时错误:

      span.innerHTML="<div>Hi</div>";          //这里span包含<div>元素

在遇到把块级元素插入到不恰当位置的情况时,其他浏览器会尝试纠正并隐藏错误,而IE会提示错误。

5.语法错误

一般情况下,只要IE报告发生了语法错误(Syntax Error),都可以很快找到错误的原因,但还有一种原因不是十分明显的情况需要格外注意。

如果用户引用了外部的JavaScript文件,而该文件最终并没有返回JavaScript代码,IE也会抛出语法错误。例如,<script>元素的src特性指向了一个HTML文件,就会导致语法错误。报告语法错误的位置时,通常都会说该错误位于脚本第一行的第一个字符处。Opera和Safari也会报告语法错误,但它们会给出导致问题的外部文件的信息,IE就不会给出这个信息,因此就需要自己重复检查一遍引用的外部JavaScript文件,但Firefox会忽略这种解析错误。

6.系统无法找到指定资源

在使用JavaScript请求某个资源URL,而该URL的长度超过了IE对URL最长不能超过2083个字符的限制时,就会发生这个错误。IE不仅限制JavaScript中使用的URL的长度,而且也限制用户在浏览器自身中使用的URL长度(其他浏览器对URL的限制没有这么严格), IE对URL路径还有一个不能超过2048个字符的限制。

【示例5】下面的代码将会导致错误。

      function createLongUrl(url){
          var s = "? ";
          for (var i=0, len=2500; i < len; i++){
            s += "a";
          }
          return url + s;
      }
      var x = new XMLHttpRequest();
      x.open("get", createLongUrl("http://www.somedomain.com/"), true);
      x.send(null);

在这个例子中,XMLHttpRequest对象试图向一个超出最大长度限制的URL发送请求。在调用open()方法时,就会发生错误。避免这个问题的办法,无非就是通过给查询字符串参数起更短的名字,或者减少不必要的数据,来缩短查询字符串的长度。另外,还可以把请求方法改为POST,通过请求体而不是查询字符串来发送数据。