[译]10个最常见的JavaScript错误

发布于 2019-08-19  约 18 分钟

为了回馈我们的开发人员社区,我们查看了数千个项目的数据库,并发现了JavaScript中的前10个错误。我们将向您展示导致它们的原因以及如何防止它们发生。如果你避免这些“陷阱”,它会让你成为一个更好的开发者。

由于数据为王,我们收集,分析并排名前十大JavaScript错误。Rollbar收集每个项目的所有错误,并总结每个项目发生的次数。我们通过根据指纹对错误进行分组来实现此目的。基本上,如果第二个错误只是第一个错误的重复,我们将两个错误分组。这为用户提供了一个很好的概述。

我们专注于最有可能影响您和您的用户的错误。为此,我们根据不同公司遇到的项目数量对错误进行排名。如果我们只查看每个错误发生的总次数,那么大批量客户可能会因为与大多数读者无关的错误而压倒数据集。

这里是10大JavaScript错误:

图片描述

为了可读性,上面错误的描述都是缩写后的。接下来会深入探讨一下,这些错误发生的原理,并且如何避免触发他们。

1. Uncaught TypeError: Cannot read property

如果你是一个JavaScript开发人员,可能你看到这个错误的次数,比你希望承认的次数还要多。当你在一个未定义undefined的对象上读取一个属性或调用一个方法时,这个错误就会在chrome里触发(当然在其他浏览器中也会报错,但是错误信息不是这样描述的)。在chrome开发者控制台console里,可以测试这个错误:
图片描述

这个错误出现的原因有很多,最常见的一种场景是:当使用UI组件进行渲染时,声明state不正确。让我们来看下下面这段在真实app中的示例代码片段。我们选取的react的代码,但是同理这种不恰当的声明在vue、Angular或其他框架中也会出现。

    class Quiz extends Component {
      componentWillMount() {
        axios.get('/thedata').then(res => {
          this.setState({items: res.data});
        });
      }
    
      render() {
        return (
          <ul>
            {this.state.items.map(item =>
              <li key={item.id}>{item.name}</li>
            )}
          </ul>
        );
      }
    }

这里有两点需要注意的:

  1. 一个组件的state(比如上面的this.state)在组件生命周期开始时是未声明的,为undefined。
  2. 当你异步获取数据时,组件在获取到数据之前,无论你获取数据的代码是写在constructor方法,还是componentWillMount或者componentDidMount的生命周期里,至少都会调用一次render方法渲染模板。上面的示例代码运行第一次render的时候,this.state.items为undefined。这意味着本该是ItemList的值,却为undefined,接着你就会在console里看到一个错误”Uncaught TypeError: Cannot read property ‘map’ of undefined“

这个问题修复起来也很简单。最简单的方法:在constructor里初始化时用恰当的默认值赋值给state。

class Quiz extends Component {
  // 在这里添加:
  constructor(props) {
    super(props);

    // 声明state本身,并给他的属性都设置上一个默认值
    this.state = {
      items: []
    };
  }

  componentWillMount() {
    axios.get('/thedata').then(res => {
      this.setState({items: res.data});
    });
  }

  render() {
    return (
      <ul>
        {this.state.items.map(item =>
          <li key={item.id}>{item.name}</li>
        )}
      </ul>
    );
  }
}

在你的app中的具体代码可能和上面有区别,但我们还是希望这会给你足够多的线索去修复或避免这个错误。如果没能帮到你,请继续阅读下面的更多例子以及相关的错误。

2.TypeError: ‘undefined’ is not an object

当你在一个未定义undefined的对象上读取一个属性或调用一个方法时,在safari里就会报这个错。你可以再Safari的console控制台里测试这个错误,本质上和上面那个在chrome中出现错误是一样的,只是在Safari用里的错误信息有区别。
图片描述

3. TypeError: null is not an object

当你去读取一个null对象的属性或调用方法时,会在Safari里出现这个错误。可以在Safari 控制台里测试这个错误。

图片描述
有趣的是,在JavaScript中的null和undefined是不相等的,所以我们才会得到不同的错误信息。Undefined通常是指一个变量没有被声明,而null表示一个变量的值为空。使用严格相等操作符可以证实他们是不相等的。
图片描述

在实际项目中有一种出现这种错误的场景:当你在js中想要操作一个dom元素,但这个元素还没加载或者不存在时。这是因为dom的API会在你查找dom元素的结果为空的情况下返回null。
任何处理dom元素的代码必须要放在dom元素被创建完毕之后。JS代码正如HTML中一样,是从上而下执行的。所以,如果你在html代码里的dom元素之前使用了一个JavaScript标签,并在里面包含了一些内联的js代码,那么这些js代码会在html页面解析之前执行。这时可能就会出现这个错误,因为在加载js代码之前,dom元素还没有被创建好。
在这种情况下,我们可以通过添加一个监听页面是否解析完毕的事件监听来解决问题。一但事件监听器触发,init()方法就能开始使用dom元素了。

<script>
  function init() {
    var myButton = document.getElementById("myButton");
    var myTextfield = document.getElementById("myTextfield");
    myButton.onclick = function() {
      var userName = myTextfield.value;
    }
  }
  document.addEventListener('readystatechange', function() {
    if (document.readyState === "complete") {
      init();
    }
  });
</script>

<form>
  <input type="text" id="myTextfield" placeholder="Type your name" />
  <input type="button" id="myButton" value="Go" />
</form>

4. (unknown): Script error

当一个未被捕获的错误在跨域时,违背了浏览器的跨域策略,就会出现这个错误。举个例子,你把js代码放在了CDN上面,任何未捕捉的错误发生时(这里指冒泡到window.onerror的监听处理器,而没有try/catch的错误)都只会报一条简单的'Script error'信息,而没有更加详细有帮助的错误信息。这是浏览器的一种安全手段,为了防止跨域传输数据,不允许进行通信。
想要获取到真实详细的错误信息,你可以像这样做:

  • 在header里添加 Access-Control-Allow-Origin 字段

    在header(这应该是服务器返回的response header)字段里,把Access-Control-Allow-Origin设为,这样就表示来自任意的域名请求都可以正确地访问到服务器的资源。必要的话也可以指定具体的域名来代替星号,比如:Access-Control-Allow-Origin: www.example.com。但是配置的域名太多的话,处理起来会有点棘手,而且如果你在使用CDN的话还会出现缓存的问题,这样就有点费力不讨好了。更多参考这里

    下面举一些在各种环境下配置这个header的示例:
    Apache:
    在JavaScript代码所在的文件夹目录下,新建一个.htaccess文件,内容如下:

    Header add Access-Control-Allow-Origin "*"

    Nginx:

    在JavaScript代码所在文件夹目录下面,添加add_header命令:

    location ~ ^/assets/ {
        add_header Access-Control-Allow-Origin *;
    }

    HAProxy:

    在后端的JavaScript所在文件加入以下内容:

    rspadd Access-Control-Allow-Origin:\ *
  • 在JavaScript标签上设置crossorigin="anonymous"

    在html代码里,每个设置好了Access-Control-Allow-Origin的js资源,都可以在其JavaScript标签上添加crossorigin="anonymous"。在设置crossorigin="anonymous"之前,确定好header字段都是正确发送了的。在Firefox里,如果js标签上出现了crossorigin属性,但是header里没有Access-Control-Allow-Origin,那么该js将不会被执行。(crossorigin是html5新增的功能,不只是JavaScript标签独有的,比如video、image也可以设置)

5. TypeError: Object doesn’t support property

这个错误发生在IE浏览器中,当你调用一个未定义的方法时,可以在IE的console里测试这个:
图片描述

这个错误和发生在chrome里的"TypeError: ‘undefined’ is not a function"是相同的,不同的浏览器对于相同的逻辑错误会给出不同的错误信息。
这是一个常见的错误,当你在IE里操作JavaScript的命名空间时。这种情况百分之九十九是因为IE无法将当前作用域的方法绑定给this关键字。举个例子,假设你有一个名叫Rollbar的作用域,里面包含了一个isAwesome函数。正常情况下,你可以用下面这样的语法在Rollbar作用域里引用isAwesome函数:

this.isAwesome();

Chrome,Firefox 和 Opera 会能接受这个语法,但是IE不行。 因此,使用 JS 命名空间时最安全的选择是始终以实际名称空间作为前缀:

Rollbar.isAwesome();

6. TypeError: ‘undefined’ is not a function

调用一个未定义的函数时会出现这个错误,可以在Chrome或Mozilla Firefox的console里测试这个:
图片描述

随着js代码的编码技巧和设计模式越来越复杂,在回调函数、闭包等各种作用域中this的指向的层级也随之增加,这就是js代码中this/that指向容易混淆的原因。
先来看下这段代码:

function testFunction() {
  this.clearLocalStorage();
  this.timer = setTimeout(function() {
    this.clearBoard();    // 这个this指向谁?
  }, 0);
};

执行上述代码时,会出现错误: "Uncaught TypeError: undefined is not a function."。这是因为你执行setTimeout方法时,其实是执行的window.setTimeout。所以作为参数传递过去的匿名函数,其实是在window作用域下执行的,而window对象并没有clearBoard方法。
一个最简单的、能兼容旧版本浏览器的方法,就是先把this指向赋值给一个变量self,然后在闭包里直接引用这个self变量。像这样:

function testFunction () {
  this.clearLocalStorage();
  var self = this;   // 把this赋值给self,这个作用域就会被保存下来
  this.timer = setTimeout(function(){
    self.clearBoard();  
  }, 0);
};

另外也可以使用bind方法来传递恰当的this指向:

function testFunction () {
  this.clearLocalStorage();
  this.timer = setTimeout(this.reset.bind(this), 0);  // bind to 'this'
};

function testFunction(){
    this.clearBoard();    //back in the context of the right 'this'!
};

7. Uncaught RangeError: Maximum call stack

在chrome中有好几个情况会触发这个错误。其中一种情况就是无终止地调用一个递归函数。
图片描述

还有当你给函数传参时,如果超出了范围,也会出现这个错误。许多函数在接收数字类型的参数时,都有一个具体的范围要求。比如,Number.toExponential(digits) 和 Number.toFixed(digits)方法,只接受0到20的数字作为参数,而Number.toPrecision(digits) 接收1到21的数字。

var a = new Array(4294967295);  //OK
var b = new Array(-1); //range error

var num = 2.555555;
document.writeln(num.toExponential(4));  //OK
document.writeln(num.toExponential(-2)); //range error!

num = 2.9999;
document.writeln(num.toFixed(2));   //OK
document.writeln(num.toFixed(25));  //range error!

num = 2.3456;
document.writeln(num.toPrecision(1));   //OK
document.writeln(num.toPrecision(22));  //range error!

8. TypeError: Cannot read property ‘length’

当在chorme中读取一个未定义变量的length属性时,就会出现这个错误。
图片描述

正常情况下你可以在数组对象上读取这个length属性,但是如果你要使用的数组对象没有被初始化,或者因为作用域的问题而没有正确地获取到,可能就会出现这个错误。来看下面这段代码理解下:

var testArray= ["Test"];

function testFunction(testArray) {
    for (var i = 0; i < testArray.length; i++) {
      console.log(testArray[i]);
    }
}

testFunction();

当你声明函数的参数时,这些参数就是在函数内部的本地参数。这意味着,你在外部声明的全局变量和本地变量同名了话(都是叫testArray),那在函数内部读取的一定是本地的变量,即传入的参数。
有两种方法解决这样的问题

  • 在函数声明时,去掉这些参数。

    var testArray = ["Test"];
    
    /* Precondition: defined testArray outside of a function */
    function testFunction(/* No params */) {
        for (var i = 0; i < testArray.length; i++) {
          console.log(testArray[i]);
        }
    }
    
    testFunction();
  • 把外部的变量作为参数正确地传给函数内部。

    var testArray = ["Test"];
    
    function testFunction(testArray) {
       for (var i = 0; i < testArray.length; i++) {
          console.log(testArray[i]);
        }
    }
    
    testFunction(testArray);

9. Uncaught TypeError: Cannot set property

当我们把一个变量为undefined的时候,它就永远返回undefined,不能再读取/设置它的属性。否则,就会抛出这个错误。
图片描述

10. ReferenceError: event is not defined

当您尝试访问未定义的变量或当前作用域无法访问到的变量时,就会出现这个错误。
图片描述

阅读 193发布于 2019-08-19

推荐阅读
由点到面
用户专栏

很多情况下,我们都是被动的在工作,在学习。在学了几年之后,发现并没有将所学的知识串联起来,故开此...

327 人关注
31 篇文章
专栏主页
目录