10个最常见的JavaScript错误[译]
前言
- 翻译自原文,省去了原文的开头和结尾,只翻译中间的主要内容——介绍10个最常见的js错误,并如何避免这些陷阱。部分地方是我在翻译时,根据自己的理解加上的标注或一些延展性的内容。
这里是10大JavaScript错误:
image.png- 为了可读性,上面错误的描述都是缩写后的。接下来会深入探讨一下,这些错误发生的原理,并且如何避免触发他们。
1. Uncaught TypeError: Cannot read property
-
如果你是一个JavaScript开发人员,可能你看到这个错误的次数,比你希望承认的次数还要多。当你在一个未定义undefined的对象上读取一个属性或调用一个方法时,这个错误就会在chrome里触发(当然在其他浏览器中也会报错,但是错误信息不是这样描述的)。在chrome开发者控制台console里,可以测试这个错误:
image.png - 这个错误出现的原因有很多,最常见的一种场景是:当使用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 (evaluating
-
当你在一个未定义undefined的对象上读取一个属性或调用一个方法时,在safari里就会报这个错。你可以再Safari的console控制台里测试这个错误,本质上和上面那个在chrome中出现错误是一样的,只是在Safari用里的错误信息有区别。
image.png
3. TypeError: null is not an object (evaluating
-
当你去读取一个null对象的属性或调用方法时,会在Safari里出现这个错误。可以在Safari 控制台里测试这个错误。
image.png -
有趣的是,在JavaScript中的null和undefined是不相等的,所以我们才会得到不同的错误信息。Undefined通常是指一个变量没有被声明,而null表示一个变量的值为空。使用严格相等操作符可以证实他们是不相等的。
image.png -
在实际项目中有一种出现这种错误的场景:当你在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>
译者注:
- 上面说的这个问题,是因为在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
- 当一个未被捕获的错误在跨域时,违背了浏览器的跨域策略,就会出现这个错误。举个例子,你把js代码放在了CDN上面,任何未捕捉的错误发生时(这里指冒泡到window.onerror的监听处理器,而没有try/catch的错误)都只会报一条简单的'Script error'信息,而没有更加详细有帮助的错误信息。这是浏览器的一种安全手段,为了防止跨域传输数据,不允许进行通信。
- 想要获取到真实详细的错误信息,你可以像这样做:
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:\ *
2. 在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里测试这个:
image.png -
这个错误和发生在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里测试这个:
image.png -
随着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中有好几个情况会触发这个错误。其中一种情况就是无终止地调用一个递归函数。
image.png - 还有当你给函数传参时,如果超出了范围,也会出现这个错误。许多函数在接收数字类型的参数时,都有一个具体的范围要求。比如,
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
image.png - 另外,这个错误的直译过来就是堆栈内存满了。可以这样理解,每次递归调用函数,就会使用一层"栈的内存",当达到浏览器默认的堆栈大小或内存耗尽时,就会触发这个错误。那么如何防止呢?可以尾调用优化,函数结尾改成尾递归,具体内容参考这里,文中提到的一个观念就是使用尾递归来避免栈溢出,遗憾的是目前js还是无法支持"尾调用优化"。
8. TypeError: Cannot read property ‘length’
-
当在chorme中读取一个未定义变量的length属性时,就会出现这个错误。
image.png - 正常情况下你可以在数组对象上读取这个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,不能再读取/设置它的属性。否则,就会抛出这个错误。
image.png
10. ReferenceError: event is not defined
-
当您尝试访问未定义的变量或当前作用域无法访问到的变量时,就会出现这个错误。
image.png