1. 程式人生 > >詳解1000+專案資料分析出來的10大JavaScript錯誤

詳解1000+專案資料分析出來的10大JavaScript錯誤

譯者按: null/undefined引發的錯誤在10大錯誤中比例很高。而它們很可能導致嚴重問題,所以要重視起來。

為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用於學習。

為了回饋擁護我們的開發者,我們將所有專案資料分析了一下,總結出10大JavaScript錯誤。我們會詳細解釋錯誤的原因以及如何預防再次發生。如果你學會了避開這些坑,那麼你將會是一個更加出色的開發者。

如今資料為王,我們聚合了大量BUG資料,並對它們進行分析,列出了排名前十的JavaScript錯誤。Rollbar收集每一個專案所有的錯誤,並統計它們發生的次數。我們將相同的錯誤聚合起來。如果同一個錯誤出現很多次的話,這樣就可以避免像日誌一樣非常多,讓人無從下手。

我們將統計同一個錯誤在多少個專案中出現,並以此來排序。如下所示:



為了方便閱讀,每一條錯誤我們將後面的內容做了適當省略。接下來我們詳細介紹每一個錯誤。

1. Uncaught TypeError: Cannot read property

如果你是一個JavaScript開發者,這種錯誤大概你已經見怪不怪了。在Chrome下,當你從一個不存在的物件(undefined)獲取屬性或則進行函式呼叫,就會報這樣的錯。你可以在Chrome瀏覽器控制檯測試:



有很多種原因可以導致這種情況的出現,一個常見的情況是在渲染UI部件的時候,沒有正確地初始化狀態(state)。我們來看一個真實的例子。在這裡我選用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>
)}
</u
l>

);
}
}

這裡有兩個關鍵點:

  • 元件的狀態(state)(this.state)沒有初始化,值為undefined
  • 如果使用非同步的方式獲取資料,那麼在資料載入前,該元件已經至少渲染一次。這和componentWillMount或則componentDidMount是否獲取資料無關。也就是說,當Quiz第一次渲染的時候,this.state.items是未定義的。因此,會報錯:"Uncaught TypeError: Cannot read property ‘map’ of undefined"

這個bug很容易修復。最簡單的方法:在建構函式中初始化state。

class Quiz extends Component {
// Added this:
constructor(props) {
super(props);

// Assign state itself, and a default value for items
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 (evaluating

在Safari下,如果在一個未定義(undefined)的物件上讀取屬性或則呼叫函式,就會觸發這樣的錯誤。你可以在Safari控制檯測試。這個錯誤根本上來說和第一個在Chrome下的錯誤是一樣的,只是錯誤的訊息不同。



備註:Fundebug早已機智地將這兩種情況聚合為一個錯誤了,更加方便分析,歡迎各位老鐵試用!

3. TypeError: null is not an object (evaluating

在Safari下,如果你嘗試從null讀取屬性或則呼叫方法,就會報錯。如下:



有趣的是,在JavaScript中,null和undefined是不同的,所以我們看到兩個不同的錯誤訊息。Undefined指的是一個變數沒有被賦值,而null指的是值為空。我們可以用===來判斷:



一種現實中可能的情況就是:如果你嘗試在一個DOM元素載入之前使用它。那麼DOM API就會返回null。任何處理DOM元素的JS程式碼都應當在DOM載入完畢之後呼叫。JS程式碼是按照程式碼的順序從上往下依次解釋執行。如果在DOM元素前有指令碼,那麼在瀏覽器分析HTML頁面的時候,JS程式碼也在執行了。如果JS程式碼執行的時候,DOM還沒有建立好,那麼你會遇到這個錯誤。

最常用的解法是使用事件監聽,當DOM載入完畢之後,再觸發JS程式碼的執行。

<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

當未捕獲的 JavaScript 錯誤(通過window.onerror處理程式引發的錯誤,而不是捕獲在try-catch中)被瀏覽器的跨域策略限制時,會產生這類的指令碼錯誤。 例如,如果您將您的 JavaScript 程式碼託管在 CDN 上,則任何未被捕獲的錯誤將被報告為“指令碼錯誤” 而不是包含有用的堆疊資訊。這是一種瀏覽器安全措施,旨在防止跨域傳遞資料,否則將不允許進行通訊。

想要獲取到真實詳細的錯誤資訊,你可以像這樣做:

  1. 下面舉一些在各種環境下配置這個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開發者控制檯中進行測試。



相當於 Chrome 中的 “TypeError:”undefined“ is not a function” 錯誤。 對於相同的錯誤,不同的瀏覽器具有不同的錯誤訊息。

在IE裡使用JavaScript的名稱空間時,就很容易碰到這個錯誤。發生這個錯誤十有八九是因為IE無法將當前名稱空間裡的方法繫結到this關鍵字上。例如,假設有個名稱空間Rollbar,它有一個方法叫isAwesome()。在Rollbar名稱空間中,可以直接使用this關鍵字來呼叫這個方法:

this.isAwesome();

在Chrome、Firefox和Opera中這樣做都是沒有問題的,但在IE中就不行。所以,最安全的做法是指定全名稱空間:

Rollbar.isAwesome();

6. TypeError: ‘undefined’ is not a function

在Chrome下,呼叫一個未定義的函式時就會發生這個錯誤,可以在Chrome/Mozilla開發者控制檯測試:



隨著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()。傳給setTimeout()的匿名函式的this實際上是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裡,有幾種情況會發生這個錯誤,其中一個就是函式的遞迴呼叫,並且不能終止。這個錯誤可以在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!

來自網友的備註

  • 我在chorme測試時,發現上述的第二種引數超出範圍的情況,錯誤資訊並不是”Maximum call stack“,並且Number.toExponential(digits) 和 Number.toFixed(digits)方法,接收的範圍應該是0到100


  • 另外,如果遞迴層數太多,會導致記憶體溢位。那麼如何防止呢?可以尾呼叫優化,函式結尾改成尾遞迴,具體內容參考這裡,文中提到的一個觀念就是使用尾遞迴來避免棧溢位,遺憾的是目前js還是無法支援”尾呼叫優化”。

8. TypeError: Cannot read property ‘length’

在Chrome中,如果讀取未定義變數的長度屬性,會報錯。



如果陣列未初始化,或者因為作用域的問題而沒有正確地獲取到,則可能會遇到此錯誤。讓我們用下面的例子來理解這個錯誤。

var testArray= ["Test"];

function testFunction(testArray) {
for (var i = 0; i < testArray.length; i++) {
console.log(testArray[i]);
}
}

testFunction();

函式的引數名會覆蓋全域性的變數名。也就是說,全域性的testArray被函式的引數名覆蓋了,所以在函式體裡訪問到的是本地的testArray,但本地並沒有定義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();
  1. 把外部的變數傳給函式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變數進行賦值或讀取操作,會丟擲“Uncaught TypeError: cannot set property of undefined”異常。



因為test物件不存在,就會丟擲“Uncaught TypeError: cannot set property of undefined”異常。

10. ReferenceError: event is not defined

當訪問一個未定義的物件或超出當前作用域的物件,就會發生這個錯誤。



如果在使用事件處理系統時遇到此錯誤,請確保使用傳入的事件物件作為引數。舊瀏覽器(IE)提供了全域性的event變數,但並不是所有的瀏覽器都支援。像jQuery這樣的庫試圖規範化這種行為。儘管如此,最好使用傳入事件處理函式的函式。

function myFunction(event) {
event = event.which || event.keyCode;
if(event.keyCode===13){
alert(event.keyCode);
}
}

結論

看到這裡,你會發現這十大錯誤幾乎都是null/undefined錯誤。如果有一個好的靜態型別檢查系統,比如使用TypeScript可以幫助你在編譯的時候就發現問題。如果沒有使用TypeScript,那麼請多多使用條件語句做判斷,防止這種情況出現。

在生產環境中會出現各種不可預期的錯誤。關鍵是要及時發現那些影響使用者體驗的錯誤,並使用適當的工具快速發現和解決這些問題。Fundebug提供網站bug監控,助你實時發現bug。

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了8億+錯誤事件,得到了Google、360、金山軟體、百姓網等眾多知名使用者的認可。歡迎免費試用!

您的使用者遇到BUG了嗎?

體驗Demo 免費使用