来自1000多个项目的10大JavaScript错误浅析
pl5438
7年前
<p>作为对社区开发者的回馈,我们从我们的数据库里选出了10大来自数千个项目的JavaScript错误。我们将会给出产生这些错误的根源,以及如何避免再发生这些错误。如果能够避免这些错误,就可以成为更好的开发者。</p> <p>数据才是王道,我们通过收集和分析大量数据才选出了这10大JavaScript错误。我们收集每一个项目中出现的错误,并统计每一个错误发生的次数。我们根据错误代码的指纹(fingerprint)对它们进行分组,也就是说,如果第二个错误与第一个是重复的,就把它们归入同一个组。这样就可以为用户提供更好的视图,而不是像查看繁琐的日志文件那样。</p> <p>我们只关注影响面最大的那些错误。为此,我们统计了错误在各个公司的项目中发生的次数,而不是错误发生的总次数,因为如果是这样的话,读者就可能看到大量与他们不相干的统计信息。</p> <p>以下是排名靠前的10大JavaScript错误:</p> <p><img src="https://simg.open-open.com/show/3626f5effefbd67ee761dcdc9c31d6a5.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="1116" height="691"></p> <p>出于可读性方面的考虑,每个错误的描述经过精简。</p> <h3>1.Uncaught TypeError: Cannot read property</h3> <p>如果你是一名JavaScript开发者,对这个错误可能已经熟视无睹。在Chrome里读取未定义对象的属性或调用未定义对象的方法时就会发生这个错误,在Chrome开发者控制台可以很容易地重现这个错误。</p> <p><img src="https://simg.open-open.com/show/74e20ddede935c7321ea594b641d2ff2.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="915" height="145"></p> <p>发生这个错误的原因有很多,其中最为常见的是,在渲染UI组件时没有正确初始化状态。我们通过一个真实的例子来看看这个错误是怎么发生的。我们选择React作为示例,不过在其他框架(Angular、Vue等)中也是一样的。</p> <pre> 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> ); } }</pre> <p>这里要注意两件事:</p> <ol> <li>组件的状态(如this.state)在一开始就是undefined。</li> <li>如果是通过异步的方式来加载数据,那么在数据加载进来之前,至少要渲染一次组件——不管是在构造器、componentWillMout()还是componentDidMout()中加载数据。Quiz在进行第一次渲染时,this.state.items是undefined,那么ItemList就会得到undefined的数据项,这样就会在控制台看到这个错误——“Uncaught TypeError:Cannot read property ‘map’ of undefined”。</li> </ol> <p>要解决这个问题其实很简单,在构造器里使用适当的默认值进行初始化。</p> <pre> 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> ); } }</pre> <h3>2. TypeError: ’undefined’ is not an object</h3> <p>在Safari里读取未定义对象的属性或调用未定义对象的方法时就会发生这个错误,在Safari开发者控制台可以很容易地重现这个错误。这个错误与发生在Chrome里的是差不多的,只是Safari为它提供了不同的错误信息。</p> <p><img src="https://simg.open-open.com/show/f45b6b3f05d9cc61ff80741a3f917599.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="575" height="160"></p> <h3>3. TypeError: null is not an object</h3> <p>在Safari里读取空(null)对象的属性或调用空对象的方法时就会发生这个错误,在Safari开发者控制台可以很容易地重现这个错误。</p> <p><img src="https://simg.open-open.com/show/0d6702e5274d3669a8316e120fcbfafc.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="555" height="159"></p> <p>有意思的是,在JavaScript里,null和undefined其实是不一样的,所以我们会看到两个不同的错误消息。undefined表示未赋值的变量,而null表示变量值为空。可以使用严格等于号来证明它们不是同一个东西。</p> <p><img src="https://simg.open-open.com/show/56ee7294651a17434e82727be31531de.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="916" height="129"></p> <p>在实际应用当中,在JavaScript里调用一个未加载的DOM元素就会出现这个错误。如果对象为空,DOM API就会返回null。</p> <p>DOM元素需要在创建之后才能被访问。JavaScript代码是按照从上到下的顺序进行解析的,所以,如果在DOM元素之前有一个标签包含了JavaScript代码,浏览器在解析HTML时就会执行这些代码。在加载JavaScript之前,如果DOM元素没有被创建,就会出现这个错误。</p> <p>在这个例子里,我们可以通过添加一个事件监听器来解决这个问题,在页面加载完毕时,事件监听器会通知我们。在addEventListener被触发之后,init()方法就可以大胆地访问DOM元素了。</p> <pre> <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></pre> <h3>4. (unknown): Script error</h3> <p>跨域的未捕捉JavaScript异常会变成Script error。例如,假设JavaScript托管在CDN上,那么未捕捉的错误(错误没有在try-catch里被捕获,一路直上到了window.onerror里)就会显示成“Script error”,而不是显示具体的错误消息。这是浏览器出于安全方面的考虑,防止跨域传递数据。</p> <p>要想获得具体的错误信息,可以这样做:</p> <p>1).使用Access-Control-Allow-Origin</p> <p>将Access-Control-Allow-Origin设置成“*”,表示该资源可以被任何一个域访问。如果有必要,可以把“*”替换成你的域名,例如Access-Control-Allow-Origin: www.example.com。不过,如果使用了CDN,那么要支持多个域名可能就会得不偿失,因为CDN存在缓存问题。</p> <p>下面是在各种环境如何设置该字段的示例:</p> <p>Apache</p> <p>在JavaScript文件所在的目录创建一个叫作.htaccess的文件,并加入如下内容:</p> <pre> Header add Access-Control-Allow-Origin “*"</pre> <p>Nginx</p> <p>在JavaScript对应的location配置代码块中加入add_header指令:</p> <pre> location ~ ^/assets/ { add_header Access-Control-Allow-Origin *; }</pre> <p>HAProxy</p> <p>在JavaScript文件对应的backend配置块中加入如下内容:</p> <pre> rspadd Access-Control-Allow-Origin:\ *</pre> <p>2). 在script标签里设置crossorigin=“anonymous”</p> <p>在每个设置了Access-Control-Allow-Origin字段的HTML页面里,将它们的script标签的crossorigin属性设置为“anonymous”。在Firefox里,如果出现了crossorigin,但没有设置Access-Control-Allow-Origin,JavaScript脚本就不会被执行。</p> <h3>5. TypeError: Object doesn’t support property</h3> <p>在IE里读取未定义对象的属性或调用未定义对象的方法时就会发生这个错误,在IE开发者控制台可以很容易地重现这个错误。</p> <p><img src="https://simg.open-open.com/show/6ffa291baeea0a8ba4ccf7b262d69bdc.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="900" height="211"></p> <p>这个错误与Chrome里的“TypeError: ‘undefined’ is not a function”是同一个东西。不同的浏览器为相同的错误提供的错误消息可能是不一样的。</p> <p>在IE里使用JavaScript的命名空间时,就很容易碰到这个错误。发生这个错误十有八九是因为IE无法将当前命名空间里的方法绑定到this关键字上。例如,假设有个命名空间Rollbar,它有一个方法叫isAwesome()。在Rollbar命名空间中,可以直接使用this关键字来调用这个方法:</p> <pre> this.isAwesome();</pre> <p>在Chrome、Firefox和Opera中这样做都是没有问题的,但在IE中就不行。所以,最安全的做法是指定全命名空间:</p> <pre> Rollbar.isAwesome();</pre> <h3>6. TypeError: ‘undefined’ is not a function</h3> <p>在Chrome里调用一个未定义的函数时就会发生这个错误,可以在Chrome开发者控制台和Mozilla开发者控制台重现这个错误。</p> <p><img src="https://simg.open-open.com/show/6ebee4cf1fdc67e2096739b583a834e3.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="937" height="190"></p> <p>近年来,JavaScript的编码技术和设计模式变得日趋复杂,回调和闭包中的自引用情况越来越普遍,让人搞不清楚代码中的this/that表示的是什么意思。</p> <p>比如下面这段代码:</p> <pre> function testFunction() { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // 这里的”this"是指什么? }, 0); };</pre> <p>执行上面的代码会出现这样的错误:“Uncaught TypeError: undefined is not a function”。因为在调用setTimeout()方法时,实际上是在调用window.setTimeout()。传给setTimeout()的匿名函数的上下文实际上是window,而window并不包含clearBoard()方法。</p> <p>对于旧浏览器,以往的解决办法是将this赋值给某个变量,然后在闭包里使用这个变量。例如:</p> <pre> function testFunction () { this.clearLocalStorage(); var self = this; // 将this赋值给self this.timer = setTimeout(function(){ self.clearBoard(); }, 0); };</pre> <p>在新浏览器中,可以使用bind()方法来传递引用:</p> <pre> function testFunction () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // 绑定到 'this' }; function testFunction(){ this.clearBoard(); // 以’this’作为上下文 };</pre> <h3>7. Uncaught RangeError: Maximum call stack</h3> <p>在Chrome里,有几种情况会发生这个错误,其中一个就是无限递归调用一个函数。这个错误可以在Chrome开发者控制台重现。</p> <p><img src="https://simg.open-open.com/show/8c9c1c85621fa6373a1687989ea8e408.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="645" height="287"></p> <p>当传给函数的值超出可接受的范围时也会出现这个错误。很多函数只接受指定范围的数值,例如,Number.toExponential(digits)和Number.toFixed(digits)只接受0到20的数值,而Number.toPrecision(digits)只接受1到21的数值。</p> <pre> 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!</pre> <h3>8. TypeError: Cannot read property ‘length’</h3> <p>在Chrome里读取undefined变量的length属性时会发生这个错误,这个错误可以在Chrome开发者控制台重现。</p> <p><img src="https://simg.open-open.com/show/b9e3e179c0ddd469c5495fd127ea5f2a.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="937" height="230"></p> <p>length是数组的属性,但如果数组没有初始化或者数组的变量名被另一个上下文隐藏起来的话,访问length属性就会发生这个错误。例如:</p> <pre> var testArray= ["Test"]; function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction();</pre> <p>函数的参数名会覆盖全局的变量名。也就是说,全局的testArray被函数的参数名覆盖了,所以在函数体里访问到的是本地的testArray,但本地并没有定义testArray,所以出现了这个错误。</p> <p>有两种方法可用于解决这个问题:</p> <p>1). 将函数的参数名移除(这就表示函数里要访问的变量已经在函数外面定义好了,所以函数不需要参数):</p> <pre> var testArray = ["Test"]; /* 前提是要在函数外面定义好testArray */ function testFunction(/* No params */) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction();</pre> <p>2). 在调用函数时将变量传递进去:</p> <pre> var testArray = ["Test"]; function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); } } testFunction(testArray);</pre> <h3>9. Uncaught TypeError: Cannot set property</h3> <p>我们无法对undefined变量进行赋值或读取操作,否则的话会抛出“Uncaught TypeError: cannot set property of undefined”异常。</p> <p>例如,在Chrome中:</p> <p><img src="https://simg.open-open.com/show/948e0dc623f31f3bdad48e915baf250d.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="939" height="232"></p> <p>如果test对象不存在,就会抛出“Uncaught TypeError: cannot set property of undefined”异常。</p> <h3>10. ReferenceError: event is not defined</h3> <p>在访问一个未定义的对象或超出当前作用域的对象时就会发生这个错误,这个错误可以在Chrome开发者控制台重现。</p> <p><img src="https://simg.open-open.com/show/62e65bddf73c6c85bd9429675e60be68.png" alt="来自1000多个项目的10大JavaScript错误浅析" width="939" height="236"></p> <p>如果在进行事件处理时遇到这个错误,请确保事件对象被作为参数传入到函数当中。旧浏览器(IE)提供了全局的event变量,但并不是所有的浏览器都会这样。尽管jQuery尝试对这种行为进行规范化,但最好还是使用传给函数的event对象:</p> <pre> function myFunction(event) { event = event.which || event.keyCode; if(event.keyCode===13){ alert(event.keyCode); } }</pre> <h2>结论</h2> <p>我们希望这些内容能够帮助大家在未来避免这些错误,解决大家的痛点。不过,即使有了这些最佳实践,在生产环境中仍然会出现各种不可预期的错误。关键是要及时发现那些影响用户体验的错误,并使用适当的工具快速解决这些问题。</p> <p>查看英文原文: <a href="/misc/goto?guid=4959757156446073212" rel="nofollow,noindex">Top 10 JavaScript errors from 1000+ projects (and how to avoid them)</a></p> <p>感谢徐川对本文的审校。</p> <p> </p> <p>来自:http://www.infoq.com/cn/articles/top-10-javascript-errors</p> <p> </p>