如今網站幾乎100%使用JavaScript。JavaScript看上去是一門十分簡單的語言,然而事實並不如此。它有很多容易被弄錯的細節,一不註意就導致BUG。 1. 錯誤的對this進行引用 在閉包或則回調中,this關鍵字的作用域很容易弄錯。舉個例子: Game.prototype.restar ...
如今網站幾乎100%使用JavaScript。JavaScript看上去是一門十分簡單的語言,然而事實並不如此。它有很多容易被弄錯的細節,一不註意就導致BUG。
1. 錯誤的對this進行引用
在閉包或則回調中,this
關鍵字的作用域很容易弄錯。舉個例子:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // 此處this指的是? }, 0); }; |
如果執行上面的代碼,我們會看到報錯:
Uncaught TypeError: undefined is not a function |
出錯的原因在於:當你調用setTimeout
函數,你實際上調用的是window.setTimeout()
。在setTimeout
中傳入的匿名函數是在window
這個對象環境下,所以this
是指向window
,但是window
並沒有clearBoard
方法。
如何解決呢?定義新的變數引用指向Game
對象的this
,然後就可以使用啦。
Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // 將this指向的對象綁定到self this.timer = setTimeout(function(){ self.clearBoard(); }, 0); }; |
或則使用bind()
函數:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // 此處this的引用正確 }; |
還有一些JavaScript中錯誤的正確處理方式,歡迎點擊參考閱讀http://www.shsxt.com/it/html5/539.html。
2. 和塊作用域(block scope)有關的BUG
在大多數程式語言中,每一個函數塊都有一個獨立的新的作用域,但是在JavaScript中並不是。例如:
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // 會輸出什麼呢? |
通常在這種情況下,調用console.log()
會輸出undefined
或則報錯。不過呢,這裡會輸出10
。在JavaScript中,即使for迴圈已經結束,變數i
依然存在,並且記錄最後的值。有些開發者會忘記這一點,然後導致許多bug。我們可以使用let
而不是for
來杜絕這一問題。
3. 記憶體泄漏
你需要監控記憶體使用量,因為泄露很難避免。記憶體泄露可能由於引用不存在的對象或則迴圈引用導致。
- 如何避免:關註對象的可訪問性(reachability)。
- 可訪問的對象:
- 現有的call stack任何位置可以訪問的對象
- 全局對象
當一個對象可以通過引用訪問到,那麼會在記憶體中保存。瀏覽器的垃圾回收器僅僅會把那些不可訪問的對象回收。
4. 混淆的相等判斷
JavaScript自動將所有在布爾環境下的變數類型轉換為布爾類型,但是可能導致bug。舉例:
// 所有都是true console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // 註意:下麵兩個也是 if ({}) // … if ([]) // … |
{}
和[]
都是對象,他們都會被轉換為true。為了防止bug出現,推薦使用===
和!==
來做比較,因為不會隱式做類型轉換。
5. 低效的DOM操作
在JavaScript中,你可以輕鬆操作DOM(添加、修改和刪除),但是開發者往往很低效地去操作。這會導致bug出現,因為這些操作非常耗費計算資源。為瞭解決這個問題,推薦使用文檔碎片(Document Fragment),如果你需要操作多個DOM元素。
6. 在for迴圈中錯誤的定義函數
舉例:
var elements = document.getElementsByTagName('input'); var n = elements.length; // 假設我們有10個元素 for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("元素編號#" + i); }; } |
如果我們有10個元素,那麼點擊任何一個元素都會顯示“元素編號#10”!因為在onclick
被調用的時候,for迴圈已經結束,因此所有的i都是10。
解法:
var elements = document.getElementsByTagName('input'); var n = elements.length; // 假設有10個元素 var makeHandler = function(num) { // outer function return function() { // inner function console.log("元素編號##" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); } |
makeHandler
在for迴圈執行的時候立即被調用,獲取到當前的值i+1
,並且存儲在變數num
中。makeHandler
返回一個函數使用num
變數,該函數被綁定到元素的點擊事件。
7. 通過原型錯誤地繼承
開發者如果沒能正確理解繼承的原理,那麼就可能寫出有bug的代碼:
BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } }; var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> 輸出'default' console.log(secondObj.name); // -> 輸出'unique' |
但是,如果我們做如下操作:
delete secondObj.name; |
那麼:
console.log(secondObj.name); // -> 輸出'undefined' |
而我們實際上想要的結果是列印預設的name。
BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default'; |
每一個BaseObject
都繼承name
屬性,並且預設值為default
。此時如果secondObj
的name
屬性被刪除掉,通過原型鏈查找會返回正確的預設值。
var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> 輸出'unique' delete thirdObj.name; console.log(thirdObj.name); // -> 輸出'default' |
8. 實例方法中的無效引用
我們來實現一個簡單的構造函數用來創建對象:
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); |
為了使用方便,我們定義變數whoAmI
來引用obj.whoAmI
:
var whoAmI = obj.whoAmI; |
列印出來看看:
console.log(whoAmI); |
控制台會輸出:
function () { console.log(this === window ? "window" : "MyObj"); } |
現在我們來對比一下兩者調用的區別:
obj.whoAmI(); // 輸出"MyObj" (和期望一致) whoAmI(); // 輸出"window" (竟然輸出了window) |
當我們把obj.whoAmI
賦值給whoAmI
的時候,這個新的變數whoAmI
是定義在全局下,因此this
指向全局的window
,而不是MyObj
。如果我們真的要獲取對MyObj
的函數的引用,需要在其作用域下。
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // 任然在obj的作用域 obj.whoAmI(); // 輸出"MyObj" obj.w(); // 輸出"MyObj" |
9. setTimeout/setInterval函數第一個參數誤用字元串
如果你將一個字元串作為setTimeout/setTimeInterval,它會被傳給函數構造函數並構建一個新的函數。該操作流程很慢而且低效,並導致bug出現。
var hello = function(){ console.log("hello, fundebug !"); } setTimeout("hello", 1000); |
一個好的替代方法就是傳入函數作為參數:
setInterval(logTime, 1000); // 將logTime函數傳入 setTimeout(function() { // 傳入一個匿名函數 logMessage(msgValue); }, 1000); |
10. 未能成功使用strict mode
使用strict model會增加很多限制條件來加強安全和防止某些錯誤的出現,如果不使用strict mode
,你就相當於少了一個得力的助手幫你避免錯誤:
- 更加容易debug
- 避免不小心定義了不該定義的全局變數
- 避免this隱式轉換
- 避免屬性名字或則參數值的重覆使用
- eval()更加安全
- 無效地使用delete會自動拋出錯誤