详解1000+项目数据分析出来的10大JavaScript错误

译者按: null/undefined 引发的错误在 10 大错误中比例很高。而它们很可能导致严重问题,所以要重视起来。

本文采用意译,版权归原作者所有

为了回馈拥护我们的开发者,我们将所有项目数据分析了一下,总结出 10 大 JavaScript 错误。我们会详细解释错误的原因以及如何预防再次发生。如果你学会了避开这些坑,那么你将会是一个更加出色的开发者。

如今数据为王,我们聚合了大量 BUG 数据,并对它们进行分析,列出了排名前十的 JavaScript 错误。Rollbar 收集每一个项目所有的错误,并统计它们发生的次数。我们将相同的错误聚合起来。如果同一个错误出现很多次的话,这样就可以避免像日志一样非常多,让人无从下手。

我们将统计同一个错误在多少个项目中出现,并以此来排序。如下所示:



为了方便阅读,每一条错误我们将后面的内容做了适当省略。接下来我们详细介绍每一个错误。

1. Uncaught TypeError: Cannot read property

如果你是一个 JavaScript 开发者,这种错误大概你已经见怪不怪了。在 Chrome 下,当你从一个不存在的对象(undefined)获取属性或则进行函数调用,就会报这样的错。你可以在 Chrome 浏览器控制台测试:



有很多种原因可以导致这种情况的出现,一个常见的情况是在渲染 UI 部件的时候,没有正确地初始化状态(state)。我们来看一个真实的例子。在这里我选用 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>
);
}
}

这里有两个关键点:

  • 组件的状态(state)(this.state)没有初始化,值为undefined
  • 如果使用异步的方式获取数据,那么在数据加载前,该组件已经至少渲染一次。这和componentWillMount或则componentDidMount是否获取数据无关。也就是说,当 Quiz 第一次渲染的时候,this.state.items是未定义的。因此,会报错:"Uncaught TypeError: Cannot read property ‘map’ of undefined"

这个 bug 很容易修复。最简单的方法:在构造函数中初始化 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

在 Safari 下,如果在一个未定义(undefined)的对象上读取属性或则调用函数,就会触发这样的错误。你可以在 Safari 控制台测试。这个错误根本上来说和第一个在 Chrome 下的错误是一样的,只是错误的消息不同。



备注:Fundebug早已机智地将这两种情况聚合为一个错误了,更加方便分析,欢迎各位老铁试用!

3. TypeError: null is not an object (evaluating

在 Safari 下,如果你尝试从 null 读取属性或则调用方法,就会报错。如下:



有趣的是,在 JavaScript 中,null 和 undefined 是不同的,所以我们看到两个不同的错误消息。Undefined 指的是一个变量没有被赋值,而 null 指的是值为空。我们可以用===来判断:



一种现实中可能的情况就是:如果你尝试在一个 DOM 元素加载之前使用它。那么 DOM API 就会返回 null。任何处理 DOM 元素的 JS 代码都应当在 DOM 加载完毕之后调用。JS 代码是按照代码的顺序从上往下依次解释执行。如果在 DOM 元素前有脚本,那么在浏览器分析 HTML 页面的时候,JS 代码也在执行了。如果 JS 代码执行的时候,DOM 还没有创建好,那么你会遇到这个错误。

最常用的解法是使用事件监听,当 DOM 加载完毕之后,再触发 JS 代码的执行。

<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>

来自网友的备注

  • 上面说的这个问题,是因为在 html 中所有资源的加载都是从上而下同步加载的,所以以前的代码规范都会有一句:”在 html 里 css 标签放上面,js 标签放下面“;包括比如 jQuery 里的 ready 方法,这些做法都是为了保证 js 代码执行的时候,页面上的 dom 元素都是创建好了的。
  • 这里我再介绍一下 defer 和 async,在外链引入 js 文件的情况,可以在 script 标签上加上 defer 或 async 修饰符,使该 js 能够异步加载,从而解决上面遇到的问题。async 表示后续的解析任务和当前 js 标签的加载任务并行执行,defer 表示该 js 标签的代码会在所有页面元素解析完成之后,DOMContentLoaded 事件触发之前执行。两者具体区别参考:https://segmentfault.com/q/1010000000640869

4. (unknown): Script error

当未捕获的 JavaScript 错误(通过 window.onerror 处理程序引发的错误,而不是捕获在 try-catch 中)被浏览器的跨域策略限制时,会产生这类的脚本错误。 例如,如果您将您的 JavaScript 代码托管在 CDN 上,则任何未被捕获的错误将被报告为“脚本错误” 而不是包含有用的堆栈信息。这是一种浏览器安全措施,旨在防止跨域传递数据,否则将不允许进行通信。

想要获取到真实详细的错误信息,你可以像这样做:

  1. 在 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:\ *
  1. 在 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 开发者控制台中进行测试。



相当于 Chrome 中的 “TypeError:”undefined“ is not a function” 错误。 对于相同的错误,不同的浏览器具有不同的错误消息。

在 IE 里使用 JavaScript 的命名空间时,就很容易碰到这个错误。发生这个错误十有八九是因为 IE 无法将当前命名空间里的方法绑定到 this 关键字上。例如,假设有个命名空间 Rollbar,它有一个方法叫 isAwesome()。在 Rollbar 命名空间中,可以直接使用 this 关键字来调用这个方法:

this.isAwesome();

在 Chrome、Firefox 和 Opera 中这样做都是没有问题的,但在 IE 中就不行。所以,最安全的做法是指定全命名空间:

Rollbar.isAwesome();

6. TypeError: ‘undefined’ is not a function

在 Chrome 下,调用一个未定义的函数时就会发生这个错误,可以在 Chrome/Mozilla 开发者控制台测试:



随着 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()。传给 setTimeout()的匿名函数的 this 实际上是 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 里,有几种情况会发生这个错误,其中一个就是函数的递归调用,并且不能终止。这个错误可以在 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!

来自网友的备注

  • 我在 chorme 测试时,发现上述的第二种参数超出范围的情况,错误信息并不是”Maximum call stack“,并且 Number.toExponential(digits) 和 Number.toFixed(digits)方法,接收的范围应该是 0 到 100


  • 另外,如果递归层数太多,会导致内存溢出。那么如何防止呢?可以尾调用优化,函数结尾改成尾递归,具体内容参考这里,文中提到的一个观念就是使用尾递归来避免栈溢出,遗憾的是目前 js 还是无法支持”尾调用优化”。

8. TypeError: Cannot read property ‘length’

在 Chrome 中,如果读取未定义变量的长度属性,会报错。



如果数组未初始化,或者因为作用域的问题而没有正确地获取到,则可能会遇到此错误。让我们用下面的例子来理解这个错误。

var testArray = ["Test"];

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

testFunction();

函数的参数名会覆盖全局的变量名。也就是说,全局的 testArray 被函数的参数名覆盖了,所以在函数体里访问到的是本地的 testArray,但本地并没有定义 testArray,所以出现了这个错误。

有两种方法可用于解决这个问题:

  1. 将函数的参数移除
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();
  1. 把外部的变量传给函数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 变量进行赋值或读取操作,会抛出“Uncaught TypeError: cannot set property of undefined”异常。



因为 test 对象不存在,就会抛出“Uncaught TypeError: cannot set property of undefined”异常。

10. ReferenceError: event is not defined

当访问一个未定义的对象或超出当前作用域的对象,就会发生这个错误。



如果在使用事件处理系统时遇到此错误,请确保使用传入的事件对象作为参数。旧浏览器(IE)提供了全局的 event 变量,但并不是所有的浏览器都支持。像 jQuery 这样的库试图规范化这种行为。尽管如此,最好使用传入事件处理函数的函数。

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

结论

看到这里,你会发现这十大错误几乎都是 null/undefined 错误。如果有一个好的静态类型检查系统,比如使用 TypeScript 可以帮助你在编译的时候就发现问题。如果没有使用 TypeScript,那么请多多使用条件语句做判断,防止这种情况出现。

在生产环境中会出现各种不可预期的错误。关键是要及时发现那些影响用户体验的错误,并使用适当的工具快速发现和解决这些问题。Fundebug提供网站 bug 监控,助你实时发现 bug。

关于Fundebug

Fundebug专注于JavaScript、微信小程序、支付宝小程序线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了80亿+错误事件。欢迎大家免费试用

您的用户遇到BUG了吗?

体验Demo 免费使用