本文由程式設計師新視界原創翻譯原文作者:rollbar英文連結:https://rollbar.com/blog/top-10-javascript-errors/
為了回饋我們的開發者社區,我們對比了數千個項目的資料庫,總結了JavaScript中最容易犯的前10個錯誤。在這篇文章中,我們將為大家展示導致錯誤發生的原因,以及如何防止錯誤。避免踏入這些「陷阱」可以幫助你成為一個更好的開發者。
因為數據才是王道,所以我們收集、分析並排名了前十位JavaScript錯誤。我們使用Rollbar收集每個項目的所有錯誤,並總結每個錯誤發生的次數。我們通過fingerprints來對錯誤進行分組。基本上,如果第二個錯誤只是第一個錯誤的重複,那麼我們會把兩個錯誤分到一組。這給了用戶一個很好的概覽,而不會像你在日誌文件中看到的超級大型垃圾場那樣雜亂無章。
我們專注於最有可能影響你和用戶的錯誤。為此,我們通過涵蓋不同公司的項目數量來對錯誤進行排位。如果我們只查看每個錯誤發生的總次數,那麼大容量的客戶可能會攜帶著與大多數讀者無關的錯誤壓倒數據集。
以下是前10個JavaScript錯誤:
為了便於閱讀,每個錯誤都縮短了。下面讓我們深入到每一個錯誤以確定錯誤發生的原因,以及應該如何避免。
1.Uncaught TypeError: Cannot read property
如果你是JavaScript開發人員,那麼你可能已經看到過這個錯誤,即便你不願意承認。當你讀取一個屬性或調用一個未定義的對象的方法時,就會在Chrome中發生此錯誤。你可以在Chrome Developer Console中輕鬆地進行測試。
發生這種情況的原因很多,常見的是因為在渲染UI組件時不正確地初始化狀態。讓我們來看一個在真實app中如何發生的例子。我們將選擇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>);}}
這裡要意識到兩件重要的事情:
組件的狀態(例如this.state)從undefined開始。當你異步獲取數據時,組件在數據加載之前至少呈現一次,而不管它是在構造函數,componentWillMount還是在componentDidMount中獲取的。當Quiz第一次呈現時,this.state.items是未定義的。這反過來又意味著ItemList將項目定義為未定義,從而導致你在控制臺中得到錯誤——Uncaught TypeError: Cannot read property 『map』 of undefined。這很容易解決。最簡單的方法是:在構造函數中用合理的默認值初始化狀態。
class Quiz extends Component {// Added this:constructor(props) {super(props);// Assign state itself, and a default value for itemsthis.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中的確切代碼可能有所不同,但我們希望可以給你足夠的線索來修復或避免這個問題。如果這不是你的app中出現的問題,那麼請繼續閱讀,因為我們將在下面涵蓋更多相關錯誤的示例。
2.TypeError: 『undefined』 is not an object (evaluating
這是當你讀取屬性或調用未定義對象上的方法時,在Safari中發生的錯誤。你可以在Safari Developer Console中輕鬆地進行測試。這與在Chrome中發生的上述錯誤基本相同,但Safari使用不同的錯誤消息。
3. TypeError: null is not an object (evaluating
這是當你讀取屬性或調用空對象上的方法時,在Safari中發生的錯誤。你可以在Safari Developer Console中輕鬆地進行測試。
有趣的是,在JavaScript中,null和undefined是不一樣的,這就是為什麼我們看到兩個不同的錯誤信息。未定義通常是一個尚未分配的變量,而null表示該值為空。要驗證它們不相等,請嘗試使用嚴格的相等運算符:
在現實世界的例子中,可能發生此錯誤的一個途徑是在元素被加載之前嘗試在JavaScript中使用DOM元素。這是因為DOM API對於空白的對象引用返回null。
任何執行和處理DOM元素的JS代碼都應在創建DOM元素後執行。JS代碼按照HTML中的布局從上到下進行解釋。所以,如果DOM元素之前有一個標籤,那麼腳本標籤內的JS代碼將在瀏覽器解析HTML頁面時執行。如果在加載腳本之前尚未創建DOM元素,則會出現此錯誤。
在這個例子中,我們可以通過添加事件監聽器來解決這個問題,此監聽器會在頁面準備好的時候通知我們。一旦addEventListener被觸發,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
當未捕獲的JavaScript錯誤違背跨同源策略而跨越域邊界時,會發生Script error。例如,如果你將你的JavaScript代碼託管在CDN上,則任何未捕獲的錯誤(通過window.onerror處理程序引發而不是在try-catch中捕獲的錯誤)都將被報告為「Script error」,而不包含有用信息。這是一種瀏覽器安全措施,旨在防止跨域傳遞數據,否則將不允許進行通信。
要獲得真正的錯誤消息,請執行以下操作:
1.發送Access-Control-Allow-Origin標頭
將Access-Control-Allow-Origin標頭設置為* 表示可以從任何域正確訪問資源。如有必要,你可以將* 替換為你的域:例如,Access-Control-Allow-Origin: www.example.com。但是,處理多個域會變得棘手,並且如果由於可能出現的緩存問題而使用CDN,那或許就不值得你付出努力。更多信息請點這裡。
下面是一些關於如何在各種環境中設置這個標頭的例子:
Apache
在JavaScript文件所在的文件夾中,使用以下內容創建一個.htaccess文件:
Header add Access-Control-Allow-Origin "*"
Nginx
將add_header指令添加到提供JavaScript文件的位置塊中:
location ~ ^/assets/ {add_header Access-Control-Allow-Origin *;}
HAProxy
將以下內容添加到提供JavaScript文件的東西的後端:
rspadd Access-Control-Allow-Origin:\ *
2.在腳本標籤上設置crossorigin =「anonymous」
在HTML原始碼中,對於設置了Access-Control-Allow-Origin標頭的每個腳本,在SCRIPT標籤上設置crossorigin="anonymous"。在腳本標籤中添加crossorigin屬性之前,請確保已驗證正在為腳本文件發送標頭。在Firefox中,如果存在crossorigin屬性,但Access-Control-Allow-Origin標頭不存在,則將不執行腳本。
5. TypeError: Object doesn’t support property
這是在調用未定義的方法時發生在IE中的錯誤。你可以在IE Developer Console中進行測試。
這相當於Chrome中的「TypeError: 『undefined』 is not a function」錯誤。是的,不同的瀏覽器對相同的邏輯錯誤具有不同的錯誤消息。
對於使用JavaScript命名空間的Web應用程式中的IE,這是一個常見的問題。在這種情況下,99.9%的問題是IE無法將當前名稱空間內的方法綁定到this關鍵字。例如,如果你有JS命名空間Rollbar和方法isAwesome。通常,如果你在Rollbar命名空間內,則可以使用以下語法調用isAwesome方法:
this.isAwesome();
Chrome、Firefox和Opera會高興地接受這個語法。但是,IE則不會。因此,使用JS命名空間時最安全的選擇是始終以實際名稱空間作為前綴。
Rollbar.isAwesome();
6. TypeError: 『undefined』 is not a function
當你調用未定義的函數時,在Chrome中就會發生此錯誤。你可以在Chrome Developer Console和Mozilla Firefox Developer Console中進行測試。
隨著JavaScript編碼技術和設計模式在過去幾年中變得越來越複雜,在回調和關閉中自引用範圍的擴散也相應增加,從而使得這種混亂成為一個相當普遍的來源。
考慮下面的代碼片段:
function testFunction() {this.clearLocalStorage();this.timer = setTimeout(function() {this.clearBoard(); // what is "this"?}, 0);};
執行上述代碼將會導致以下錯誤:「Uncaught TypeError: undefined is not a function」。得到上述錯誤的原因是,當你調用setTimeout()時,實際上是調用window.setTimeout()。因此,在窗口對象的上下文中定義了一個正被傳遞給setTimeout()的匿名函數,該函數沒有clearBoard()方法。
一個傳統的,與舊瀏覽器兼容的解決方案是在一個變量中簡單地將引用保存到this,然後可以由閉包繼承。例如:
function testFunction () {this.clearLocalStorage();var self = this; // save reference to 'this', while it's still this!this.timer = setTimeout(function(){self.clearBoard();}, 0);};
或者,在較新的瀏覽器中,你可以使用bind()方法傳遞合理的引用:
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 Developer Console中進行測試。
如果你將值傳遞給超出範圍的函數,也可能發生這種情況。許多函數對於輸入值只接受特定範圍的數字。例如,Number.toExponential(digits)和Number.toFixed(digits)接受從0到20的數字,Number.toPrecision(digits)接受從1到21的數字。
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中發生的錯誤,原因是因為讀取未定義變量的長度屬性。你可以在Chrome Developer Console中進行測試。
你通常會在數組上找到定義的長度,但是如果數組未初始化或者變量名稱隱藏在另一個上下文中,則可能會遇到此錯誤。讓我們通過下面的例子來理解這個錯誤。
var testArray= ["Test"];function testFunction(testArray) {for (var i = 0; i < testArray.length; i++) {console.log(testArray[i]);}}testFunction();
當你用參數聲明一個函數時,這些參數會變成本地參數。這意味著即使你有名稱為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();
2)調用傳遞給我們聲明的數組的函數:
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的屬性。在這種情況下,應用程式將拋出「Uncaught TypeError cannot set property of undefined」。
例如,在Chrome瀏覽器中:
Uncaught TypeError屏幕截圖:無法設置屬性
如果test對象不存在,將拋出「Uncaught TypeError cannot set property of undefined」錯誤。
10. ReferenceError: event is not defined
當你嘗試訪問未定義的變量或超出當前範圍的變量時,會引發此錯誤。你可以在Chrome瀏覽器中輕鬆地進行測試。
如果你在使用事件處理系統時遇到此錯誤,那麼請確保使用傳入的事件對象作為參數。像IE這樣的老瀏覽器提供了一個全局變量事件,但並不是所有瀏覽器都支持。例如jQuery這樣的庫採取的是試圖規範化這種行為。儘管如此,最好的做法是使用傳入到事件處理函數的對象。
function myFunction(event) {event = event.which || event.keyCode;if(event.keyCode===13){alert(event.keyCode);}}
結論
我們希望你可以從這篇文章中學到新的東西,幫助避免錯誤,或者希望本指南幫助解決了令你頭痛的問題。儘管如此,即使有最佳實踐做法,生產中也依然會出現意想不到的錯誤。查看影響用戶的錯誤,並擁有快速解決問題的好工具,這一點非常重要。