为了回馈我们的开发人员社区,我们查看了数千个项目的数据库,并发现了 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>
);
}
}
这里有两点需要注意的:
- 一个组件的 state(比如上面的 this.state)在组件生命周期开始时是未声明的,为 undefined。
- 当你异步获取数据时,组件在获取到数据之前,无论你获取数据的代码是写在 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
当您尝试访问未定义的变量或当前作用域无法访问到的变量时,就会出现这个错误。