來(lái)自1000多個(gè)項(xiàng)目的10大JavaScript錯(cuò)誤淺析
作為對(duì)社區(qū)開發(fā)者的回饋,我們從我們的數(shù)據(jù)庫(kù)里選出了10大來(lái)自數(shù)千個(gè)項(xiàng)目的JavaScript錯(cuò)誤。我們將會(huì)給出產(chǎn)生這些錯(cuò)誤的根源,以及如何避免再發(fā)生這些錯(cuò)誤。如果能夠避免這些錯(cuò)誤,就可以成為更好的開發(fā)者。
數(shù)據(jù)才是王道,我們通過(guò)收集和分析大量數(shù)據(jù)才選出了這10大JavaScript錯(cuò)誤。我們收集每一個(gè)項(xiàng)目中出現(xiàn)的錯(cuò)誤,并統(tǒng)計(jì)每一個(gè)錯(cuò)誤發(fā)生的次數(shù)。我們根據(jù)錯(cuò)誤代碼的指紋(fingerprint)對(duì)它們進(jìn)行分組,也就是說(shuō),如果第二個(gè)錯(cuò)誤與第一個(gè)是重復(fù)的,就把它們歸入同一個(gè)組。這樣就可以為用戶提供更好的視圖,而不是像查看繁瑣的日志文件那樣。
我們只關(guān)注影響面最大的那些錯(cuò)誤。為此,我們統(tǒng)計(jì)了錯(cuò)誤在各個(gè)公司的項(xiàng)目中發(fā)生的次數(shù),而不是錯(cuò)誤發(fā)生的總次數(shù),因?yàn)槿绻沁@樣的話,讀者就可能看到大量與他們不相干的統(tǒng)計(jì)信息。
以下是排名靠前的10大JavaScript錯(cuò)誤:
出于可讀性方面的考慮,每個(gè)錯(cuò)誤的描述經(jīng)過(guò)精簡(jiǎn)。
1.Uncaught TypeError: Cannot read property如果你是一名JavaScript開發(fā)者,對(duì)這個(gè)錯(cuò)誤可能已經(jīng)熟視無(wú)睹。在Chrome里讀取未定義對(duì)象的屬性或調(diào)用未定義對(duì)象的方法時(shí)就會(huì)發(fā)生這個(gè)錯(cuò)誤,在Chrome開發(fā)者控制臺(tái)可以很容易地重現(xiàn)這個(gè)錯(cuò)誤。
發(fā)生這個(gè)錯(cuò)誤的原因有很多,其中最為常見(jiàn)的是,在渲染UI組件時(shí)沒(méi)有正確初始化狀態(tài)。我們通過(guò)一個(gè)真實(shí)的例子來(lái)看看這個(gè)錯(cuò)誤是怎么發(fā)生的。我們選擇React作為示例,不過(guò)在其他框架(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> ); }}
這里要注意兩件事:
組件的狀態(tài)(如this.state)在一開始就是undefined。 如果是通過(guò)異步的方式來(lái)加載數(shù)據(jù),那么在數(shù)據(jù)加載進(jìn)來(lái)之前,至少要渲染一次組件——不管是在構(gòu)造器、componentWillMout()還是componentDidMout()中加載數(shù)據(jù)。Quiz在進(jìn)行第一次渲染時(shí),this.state.items是undefined,那么ItemList就會(huì)得到undefined的數(shù)據(jù)項(xiàng),這樣就會(huì)在控制臺(tái)看到這個(gè)錯(cuò)誤——“Uncaught TypeError:Cannot read property ‘map’ of undefined”。要解決這個(gè)問(wèn)題其實(shí)很簡(jiǎn)單,在構(gòu)造器里使用適當(dāng)?shù)哪J(rèn)值進(jìn)行初始化。
class Quiz extends Component { // 增加這個(gè): constructor(props) { super(props); // 使用空數(shù)組給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> ); }} 2. TypeError: ’undefined’ is not an object
在Safari里讀取未定義對(duì)象的屬性或調(diào)用未定義對(duì)象的方法時(shí)就會(huì)發(fā)生這個(gè)錯(cuò)誤,在Safari開發(fā)者控制臺(tái)可以很容易地重現(xiàn)這個(gè)錯(cuò)誤。這個(gè)錯(cuò)誤與發(fā)生在Chrome里的是差不多的,只是Safari為它提供了不同的錯(cuò)誤信息。
在Safari里讀取空(null)對(duì)象的屬性或調(diào)用空對(duì)象的方法時(shí)就會(huì)發(fā)生這個(gè)錯(cuò)誤,在Safari開發(fā)者控制臺(tái)可以很容易地重現(xiàn)這個(gè)錯(cuò)誤。
有意思的是,在JavaScript里,null和undefined其實(shí)是不一樣的,所以我們會(huì)看到兩個(gè)不同的錯(cuò)誤消息。undefined表示未賦值的變量,而null表示變量值為空。可以使用嚴(yán)格等于號(hào)來(lái)證明它們不是同一個(gè)東西。
在實(shí)際應(yīng)用當(dāng)中,在JavaScript里調(diào)用一個(gè)未加載的DOM元素就會(huì)出現(xiàn)這個(gè)錯(cuò)誤。如果對(duì)象為空,DOM API就會(huì)返回null。
DOM元素需要在創(chuàng)建之后才能被訪問(wèn)。JavaScript代碼是按照從上到下的順序進(jìn)行解析的,所以,如果在DOM元素之前有一個(gè)標(biāo)簽包含了JavaScript代碼,瀏覽器在解析HTML時(shí)就會(huì)執(zhí)行這些代碼。在加載JavaScript之前,如果DOM元素沒(méi)有被創(chuàng)建,就會(huì)出現(xiàn)這個(gè)錯(cuò)誤。
在這個(gè)例子里,我們可以通過(guò)添加一個(gè)事件監(jiān)聽器來(lái)解決這個(gè)問(wèn)題,在頁(yè)面加載完畢時(shí),事件監(jiān)聽器會(huì)通知我們。在addEventListener被觸發(fā)之后,init()方法就可以大膽地訪問(wèn)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' placeholder='Type your name' /> <input type='button' value='Go' /></form> 4. (unknown): Script error
跨域的未捕捉JavaScript異常會(huì)變成Script error。例如,假設(shè)JavaScript托管在CDN上,那么未捕捉的錯(cuò)誤(錯(cuò)誤沒(méi)有在try-catch里被捕獲,一路直上到了window.onerror里)就會(huì)顯示成“Script error”,而不是顯示具體的錯(cuò)誤消息。這是瀏覽器出于安全方面的考慮,防止跨域傳遞數(shù)據(jù)。
要想獲得具體的錯(cuò)誤信息,可以這樣做:
1).使用Access-Control-Allow-Origin
將Access-Control-Allow-Origin設(shè)置成“*”,表示該資源可以被任何一個(gè)域訪問(wèn)。如果有必要,可以把“*”替換成你的域名,例如Access-Control-Allow-Origin: www.example.com。不過(guò),如果使用了CDN,那么要支持多個(gè)域名可能就會(huì)得不償失,因?yàn)镃DN存在緩存問(wèn)題。
下面是在各種環(huán)境如何設(shè)置該字段的示例:
Apache
在JavaScript文件所在的目錄創(chuàng)建一個(gè)叫作.htaccess的文件,并加入如下內(nèi)容:
Header add Access-Control-Allow-Origin “*'
Nginx
在JavaScript對(duì)應(yīng)的location配置代碼塊中加入add_header指令:
location ~ ^/assets/ { add_header Access-Control-Allow-Origin *;}
HAProxy
在JavaScript文件對(duì)應(yīng)的backend配置塊中加入如下內(nèi)容:
rspadd Access-Control-Allow-Origin: *
2). 在script標(biāo)簽里設(shè)置crossorigin=“anonymous”
在每個(gè)設(shè)置了Access-Control-Allow-Origin字段的HTML頁(yè)面里,將它們的script標(biāo)簽的crossorigin屬性設(shè)置為“anonymous”。在Firefox里,如果出現(xiàn)了crossorigin,但沒(méi)有設(shè)置Access-Control-Allow-Origin,JavaScript腳本就不會(huì)被執(zhí)行。
5. TypeError: Object doesn’t support property在IE里讀取未定義對(duì)象的屬性或調(diào)用未定義對(duì)象的方法時(shí)就會(huì)發(fā)生這個(gè)錯(cuò)誤,在IE開發(fā)者控制臺(tái)可以很容易地重現(xiàn)這個(gè)錯(cuò)誤。
這個(gè)錯(cuò)誤與Chrome里的“TypeError: ‘undefined’ is not a function”是同一個(gè)東西。不同的瀏覽器為相同的錯(cuò)誤提供的錯(cuò)誤消息可能是不一樣的。
在IE里使用JavaScript的命名空間時(shí),就很容易碰到這個(gè)錯(cuò)誤。發(fā)生這個(gè)錯(cuò)誤十有八九是因?yàn)镮E無(wú)法將當(dāng)前命名空間里的方法綁定到this關(guān)鍵字上。例如,假設(shè)有個(gè)命名空間Rollbar,它有一個(gè)方法叫isAwesome()。在Rollbar命名空間中,可以直接使用this關(guān)鍵字來(lái)調(diào)用這個(gè)方法:
this.isAwesome();
在Chrome、Firefox和Opera中這樣做都是沒(méi)有問(wèn)題的,但在IE中就不行。所以,最安全的做法是指定全命名空間:
Rollbar.isAwesome(); 6. TypeError: ‘undefined’ is not a function
在Chrome里調(diào)用一個(gè)未定義的函數(shù)時(shí)就會(huì)發(fā)生這個(gè)錯(cuò)誤,可以在Chrome開發(fā)者控制臺(tái)和Mozilla開發(fā)者控制臺(tái)重現(xiàn)這個(gè)錯(cuò)誤。
近年來(lái),JavaScript的編碼技術(shù)和設(shè)計(jì)模式變得日趨復(fù)雜,回調(diào)和閉包中的自引用情況越來(lái)越普遍,讓人搞不清楚代碼中的this/that表示的是什么意思。
比如下面這段代碼:
function testFunction() { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // 這里的”this'是指什么? }, 0);};
執(zhí)行上面的代碼會(huì)出現(xiàn)這樣的錯(cuò)誤:“Uncaught TypeError: undefined is not a function”。因?yàn)樵谡{(diào)用setTimeout()方法時(shí),實(shí)際上是在調(diào)用window.setTimeout()。傳給setTimeout()的匿名函數(shù)的上下文實(shí)際上是window,而window并不包含clearBoard()方法。
對(duì)于舊瀏覽器,以往的解決辦法是將this賦值給某個(gè)變量,然后在閉包里使用這個(gè)變量。例如:
function testFunction () { this.clearLocalStorage(); var self = this; // 將this賦值給self this.timer = setTimeout(function(){ self.clearBoard(); }, 0);};
在新瀏覽器中,可以使用bind()方法來(lái)傳遞引用:
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里,有幾種情況會(huì)發(fā)生這個(gè)錯(cuò)誤,其中一個(gè)就是無(wú)限遞歸調(diào)用一個(gè)函數(shù)。這個(gè)錯(cuò)誤可以在Chrome開發(fā)者控制臺(tái)重現(xiàn)。
當(dāng)傳給函數(shù)的值超出可接受的范圍時(shí)也會(huì)出現(xiàn)這個(gè)錯(cuò)誤。很多函數(shù)只接受指定范圍的數(shù)值,例如,Number.toExponential(digits)和Number.toFixed(digits)只接受0到20的數(shù)值,而Number.toPrecision(digits)只接受1到21的數(shù)值。
var a = new Array(4294967295); //OKvar b = new Array(-1); //range errorvar num = 2.555555;document.writeln(num.toExponential(4)); //OKdocument.writeln(num.toExponential(-2)); //range error!num = 2.9999;document.writeln(num.toFixed(2)); //OKdocument.writeln(num.toFixed(25)); //range error!num = 2.3456;document.writeln(num.toPrecision(1)); //OKdocument.writeln(num.toPrecision(22)); //range error! 8. TypeError: Cannot read property ‘length’
在Chrome里讀取undefined變量的length屬性時(shí)會(huì)發(fā)生這個(gè)錯(cuò)誤,這個(gè)錯(cuò)誤可以在Chrome開發(fā)者控制臺(tái)重現(xiàn)。
length是數(shù)組的屬性,但如果數(shù)組沒(méi)有初始化或者數(shù)組的變量名被另一個(gè)上下文隱藏起來(lái)的話,訪問(wèn)length屬性就會(huì)發(fā)生這個(gè)錯(cuò)誤。例如:
var testArray= ['Test'];function testFunction(testArray) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); }}testFunction();
函數(shù)的參數(shù)名會(huì)覆蓋全局的變量名。也就是說(shuō),全局的testArray被函數(shù)的參數(shù)名覆蓋了,所以在函數(shù)體里訪問(wèn)到的是本地的testArray,但本地并沒(méi)有定義testArray,所以出現(xiàn)了這個(gè)錯(cuò)誤。
有兩種方法可用于解決這個(gè)問(wèn)題:
1). 將函數(shù)的參數(shù)名移除(這就表示函數(shù)里要訪問(wèn)的變量已經(jīng)在函數(shù)外面定義好了,所以函數(shù)不需要參數(shù)):
var testArray = ['Test'];/* 前提是要在函數(shù)外面定義好testArray */function testFunction(/* No params */) { for (var i = 0; i < testArray.length; i++) { console.log(testArray[i]); }}testFunction();
2). 在調(diào)用函數(shù)時(shí)將變量傳遞進(jìn)去:
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
我們無(wú)法對(duì)undefined變量進(jìn)行賦值或讀取操作,否則的話會(huì)拋出“Uncaught TypeError: cannot set property of undefined”異常。
例如,在Chrome中:
如果test對(duì)象不存在,就會(huì)拋出“Uncaught TypeError: cannot set property of undefined”異常。
10. ReferenceError: event is not defined在訪問(wèn)一個(gè)未定義的對(duì)象或超出當(dāng)前作用域的對(duì)象時(shí)就會(huì)發(fā)生這個(gè)錯(cuò)誤,這個(gè)錯(cuò)誤可以在Chrome開發(fā)者控制臺(tái)重現(xiàn)。
如果在進(jìn)行事件處理時(shí)遇到這個(gè)錯(cuò)誤,請(qǐng)確保事件對(duì)象被作為參數(shù)傳入到函數(shù)當(dāng)中。舊瀏覽器(IE)提供了全局的event變量,但并不是所有的瀏覽器都會(huì)這樣。盡管jQuery嘗試對(duì)這種行為進(jìn)行規(guī)范化,但最好還是使用傳給函數(shù)的event對(duì)象:
function myFunction(event) { event = event.which || event.keyCode; if(event.keyCode===13){ alert(event.keyCode); }} 結(jié)論
我們希望這些內(nèi)容能夠幫助大家在未來(lái)避免這些錯(cuò)誤,解決大家的痛點(diǎn)。不過(guò),即使有了這些最佳實(shí)踐,在生產(chǎn)環(huán)境中仍然會(huì)出現(xiàn)各種不可預(yù)期的錯(cuò)誤。關(guān)鍵是要及時(shí)發(fā)現(xiàn)那些影響用戶體驗(yàn)的錯(cuò)誤,并使用適當(dāng)?shù)墓ぞ呖焖俳鉀Q這些問(wèn)題。
查看英文原文: Top 10 JavaScript errors from 1000+ projects (and how to avoid them)
感謝徐川對(duì)本文的審校。
來(lái)自:http://www.infoq.com/cn/articles/top-10-javascript-errors
相關(guān)文章:
1. css代碼優(yōu)化的12個(gè)技巧2. jsp實(shí)現(xiàn)登錄驗(yàn)證的過(guò)濾器3. JavaWeb Servlet中url-pattern的使用4. 詳解瀏覽器的緩存機(jī)制5. 微信開發(fā) 網(wǎng)頁(yè)授權(quán)獲取用戶基本信息6. msxml3.dll 錯(cuò)誤 800c0019 系統(tǒng)錯(cuò)誤:-2146697191解決方法7. asp批量添加修改刪除操作示例代碼8. jsp+servlet簡(jiǎn)單實(shí)現(xiàn)上傳文件功能(保存目錄改進(jìn))9. HTML5 Canvas繪制圖形從入門到精通10. ASP中實(shí)現(xiàn)字符部位類似.NET里String對(duì)象的PadLeft和PadRight函數(shù)
