1000+ 项目中出现最多的 10 个 JavaScript 错误(及如何避免)

本文翻译自 Top 10 JavaScript errors from 1000+ projects (and how to avoid them) , 作者 JASON SKOWRONSKI, 翻译:Thomas Chan

为了回馈我们社区的开发者,我们查询了我们数据库上的上千个项目,找到了 10 个出现最多的 JavaScript 错误。本文将告诉你是什么造成了这些错误以及如何避免,避免这些错误将使你成为一个更好的开发者。

数据是王道,我们使用了精确的处理程序 Rollbar 收集并整理了出现最多的 10 个错误,Rollbar 会收集每个项目的所有错误,通过对错误指纹分组统计出每种错误出现的次数,分组会将重复的错误去重,查看起来相比一大堆日志的体验要好很多。

我们只关注那些影响你和你们公司的错误,所以我们按照不同公司的项目对出现的错误进行排名,如果只对错误数量进行排名的话可能有些错误跟大部分读者都没什么关系。

这是出现最多的 10 个错误:
Top 10 errors
为了可读性已将每种错误摘出关键部分,让我们来深入看看什么会导致这些错误及如何避免。

1. Uncaught TypeError: Cannot read property (无法读取属性)

如果你是 JavaScript 开发者的话,这个错误你肯定已经见过很多次了。在 Chrome 里当从 undefined 或 null 读取属性或调用方法的时候会发生这个错误,你可以在 Chrome 开发者工具控制台测试一下:

var foo;
// undefined
foo.bar
// Uncaught TypeError: Cannot read property 'bar' of undefined

出现这个错有很多原因,通常情况下都是因为渲染页面时初始化数据不正确,我们来通过 React 的一个例子来看看如何复现这个错误(如果不正确的初始化数据,使用 Angular、Vue 等其他框架一样也会出现这个错误)。

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 去请求数据。当 Quiz 第一次渲染时,this.state.items 会直接报错 Uncaught TypeError: Cannot read property 'items' of undefined
    这个错误很好修改,在 constructor 里给 state 初始化合适的值就好了:
    class Quiz extends Component {
      // Added this:
      constructor(props) {
        super(props);
    
        // Assign state itself, and a default value for items
        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>
        );
      }
    }
    你的代码可能与例子不尽相同,不过希望能给你提供点思路避免这个错误,如果没有请继续看看下面其他的错误。

2. TypeError: ‘undefined’ is not an object (evaluating (undefined 不是一个对象)

这个错误与第一个错误是同一个错误,但是是出现在 Safari 浏览器里,且是从 undefined 上读取属性或方法,你可以在 Safari 开发者控制台测试下。

var a
// undefined
a.b
// TypeError: undefined is not an object (evaluating 'a.b')

3. TypeError: null is not an object (evaluating (null 不是一个对象)

这个错误也与第一个错误相同,也是出现在 Safari 浏览器,且是从 null 上读取属性或方法。

var a = null;
// undefined
a.b
// TypeError: null is not an object (evaluating 'a.b')

比较奇葩的是 JavaScript 里的 nullundefined 不是相等的,所以 Safari 抛了两种错误。

undefined == null
// true
undefined === null
// false

如果你还不太熟悉 null 与 undefined 的区别,可以看我之前的文章 null vs undefined

这个错误可能发生的场景之一是 DOM 还未加载完成,加载完成之前 DOM API 只会返回 null。所有处理 DOM 相关的 js 代码都应该在 DOM 加载完成之后执行,否则就有可能会出现这个错误。
像下面的例子,我们可以添加监听事件来通知我们页面加载完成了, addEventListener 触发时就可以调用 init 方法了。

<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 上,那 js 里没有被 try-catch 捕获的异常都会报 Script Error 而不是具体的错误信息,这是浏览器的跨源策略导致的。
可以通过以下方法拿到具体的报错信息:

1. 设置 Access-Control-Allow-Origin

Access-Control-Allow-Origin 设置为 *,即允许接收任何域名下的资源,你可以把 * 替换成你所需要的域名,比如 Access-Control-Allow-Origin: www.example.com,但是 Access-Control-Allow-Origin 是只能设置一个域名的,要设置多个域名是比较麻烦的(参考这里)。

2. 给 script 标签设置 crossorigin=”anonymous” 属性

在你的 html 里给跨源的 script 设置 crossorigin="anonymous" 属性,不过要先确定设置了 Access-Control-Allow-Origin,在 firefox 如果设置了 crossorigin 但是没设置 Access-Control-Allow-Origin 的话,该 js 脚本是不会执行的。

5. TypeError: Object doesn’t support property (对象不支持的属性)

在 IE 里调用一个不存在的函数时会出现这个错误,你可以在 IE 的开发者控制台测试一下:

this.isAwesome()
// Object doesn't support property or method 'isAwesome'

这个错误与 Chrome 里的 TypeError: 'undefined' is not a function 是同一个,不过是不同浏览器之间的相同错误的错误信息不太一致。
IE 的一个毛病是没有正确处理命名空间,所以 99.9% 的情况下这个错误都是因为 IE 没有正确的绑定命名空间给 this 导致的。比如你有一个命名空间是 Rollbar,里边有一个方法是 isAwesome,通常你可以这么调用:

this.isAwesome();

Chrome,Firefox 和 Opera 都能正确处理,但是 IE 不会。所以保险起见还是使用命名空间的写法:

Rollbar.isAwesome();

6. TypeError: ‘undefined’ is not function (undefined 不是一个函数)

这个错误是在 Chrome 里调用一个不存在的函数,你可以在 Chrome 和 Firefox 控制台测试一下:

this.foo()
// Uncaught TypeError: this.foo is not a function

随着 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(),传递给 setTimeout() 的匿名函数的上下文实际上是绑定在 window 上的,而 window 上是没有 clearBoard() 方法的。
兼容的写法是将 this 赋值给一个变量,然后在匿名函数里引用形成闭包。例如:

function testFunction () {
  this.clearLocalStorage();
  var self = this;   // 将 'this' 指向 self
  this.timer = setTimeout(function(){
    self.clearBoard();
  }, 0);
};

或者在支持 bind() 方法的浏览器里可以通过 bind() 传递引用。

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

function testFunction(){
    this.clearBoard();    // 'this' 的上下文会绑定过来
};

7. Uncaught RangeError: Maximum call stack (栈溢出)

在 Chrome 里有几种情况会出现这个错误,一种情况是递归没有被终止,你可以在 Chrome 开发者控制台测试一下:

var a = new Array(1);
function recurse(a) {
  a[0] = new Array(1);
  recurse(a[0]);
}

recurse(a);
// Uncaught RangeError: Maximum call stack size exceeded

还有一种情况是传给函数的参数超出范围了,有很多函数只接受特定范围的参数,比如 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’(无法读取 length 属性)

在 Chrome 里读取变量值为 undefinednull 的 length 属性会报这个错误。

var myButton = undefined;
myButton.length;
// UnCaught TypeError: Cannot read property 'length' of undefined

一般情况下是读取数组的长度,但是有时可能数组还没初始化或不在当前上下文,就会出现这个错。来看下边这个例子:

var testArray= ["Test"];

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

testFunction();

当你定义一个函数,且有一个参数,那这个参数就只存在于这个函数的作用域内,即使你有一个变量 testArray 跟参数名字一样。
有两个解决方法:

  1. 不给函数设置参数:
    var testArray = ["Test"];
    
    /* 前提: 在函数外定义 testArray 变量 */
    function testFunction(/* No params */) {
        for (var i = 0; i < testArray.length; i++) {
          console.log(testArray[i]);
        }
    }
    
    testFunction();
  2. 传递参数给函数
    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(无法设置属性)

我们是无法读取和设置一个变量值为 undefinednull 的属性的,否则就会抛出 Uncaught TypeError: cannot set property of undefined.
例如在 Chrome 里:

var test = undefined;
test.value = 0;
// Uncaught TypeError: cannot set property 'value' of undefined

如果 test 对象不存在,就报这个错。

10. ReferenceError: event is not defined (event 没有声明)

这个错误是尝试访问一个不在当前作用域下的变量,或者变量未声明。简单的测试一下:

function testFuncion() {
  var foo;
}
console.log(foo);

如果你是在代码里处理浏览器事件(event)的时候遇到这个错误,确保你是把 event 作为参数传递给函数的。老浏览器比如 IE 有一个全局的 event,但并不是所有浏览器都有,jQuery 库倒是尝试提供了这个行为,不过最好还是传递 event 给函数比较好。

function myFunction(event) {
    event = event.which || event.keyCode;
    if(event.keyCode===13){
       alert(event.keyCode);
    }
}

总结

希望你通过这篇文章学到了点新东西,以后能尽量避免这些错误,或者正好解决了你的燃眉之急。不论如何,即使有最佳实践,在生产环境里还是有很多意料之外的错误。重要的是能找到那些具体的错误且能很快的解决它们。