10個JavaScript常見BUG及修復方法
譯者按:JavaScript語言設計太靈活,用起來不免要多加小心掉進坑里面。
原文: Top 10 bugs and their bug fixing
譯者: Fundebug
為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用于學習。
如今網站幾乎100%使用JavaScript。JavaScript看上去是一門十分簡單的語言,然而事實并不如此。它有很多容易被弄錯的細節(jié),一不注意就導致BUG。
1. 錯誤的對this進行引用
在閉包或則回調中, this
關鍵字的作用域很容易弄錯。舉個例子:
Game.prototype.restart = function () {
this.clearLocalStorage();
this.timer = setTimeout(function() {
this.clearBoard(); // 此處this指的是?
}, 0);
};
如果執(zhí)行上面的代碼,我們會看到報錯:
Uncaught TypeError: undefined is not a function
出錯的原因在于:當你調用 setTimeout
函數(shù),你實際上調用的是 window.setTimeout()
。在 setTimeout
中傳入的匿名函數(shù)是在 window
這個對象環(huán)境下,所以 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()
函數(shù):
Game.prototype.restart = function () {
this.clearLocalStorage();
this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this'
};
Game.prototype.reset = function(){
this.clearBoard(); // 此處this的引用正確
};
2. 和塊作用域(block scope)有關的BUG
在大多數(shù)程序語言中,每一個函數(shù)塊都有一個獨立的新的作用域,但是在JavaScript中并不是。例如:
for (var i = 0; i < 10; i++) {
/* ... */
}
console.log(i); // 會輸出什么呢?
通常在這種情況下,調用 console.log()
會輸出 undefined
或則報錯。不過呢,這里會輸出 10
。在JavaScript中,即使for循環(huán)已經結束,變量 i
依然存在,并且記錄最后的值。有些開發(fā)者會忘記這一點,然后導致許多bug。我們可以使用 let
而不是 for
來杜絕這一問題。
3. 內存泄漏
你需要監(jiān)控內存使用量,因為泄露很難避免。內存泄露可能由于引用不存在的對象或則循環(huán)引用導致。
- 如何避免:關注對象的可訪問性(reachability)。
-
可訪問的對象:
- 現(xiàn)有的call stack任何位置可以訪問的對象
- 全局對象
當一個對象可以通過引用訪問到,那么會在內存中保存。瀏覽器的垃圾回收器僅僅會把那些不可訪問的對象回收。
4. 混淆的相等判斷
JavaScript自動將所有在布爾環(huán)境下的變量類型轉換為布爾類型,但是可能導致bug。舉例:
// 所有都是true
console.log(false == '0');
console.log(null == undefined);
console.log(" \t\r\n" == 0);
console.log('' == 0);
// 注意:下面兩個也是
if ({}) // …
if ([]) // …
{}
和 []
都是對象,他們都會被轉換為true。為了防止bug出現(xiàn),推薦使用 ===
和 !==
來做比較,因為不會隱式做類型轉換。
5. 低效的DOM操作
在JavaScript中,你可以輕松操作DOM(添加、修改和刪除),但是開發(fā)者往往很低效地去操作。這會導致bug出現(xiàn),因為這些操作非常耗費計算資源。為了解決這個問題,推薦使用文檔碎片(Document Fragment),如果你需要操作多個DOM元素。
廣告: 你的線上代碼真的沒有BUG嗎?歡迎免費使用 Fundebug !我們可以幫助您第一時間發(fā)現(xiàn)BUG!
6. 在for循環(huán)中錯誤的定義函數(shù)
舉例:
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循環(huán)已經結束,因此所有的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循環(huán)執(zhí)行的時候立即被調用,獲取到當前的值 i+1
,并且存儲在變量 num
中。 makeHandler
返回一個函數(shù)使用 num
變量,該函數(shù)被綁定到元素的點擊事件。
7. 通過原型錯誤地繼承
開發(fā)者如果沒能正確理解繼承的原理,那么就可能寫出有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. 實例方法中的無效引用
我們來實現(xiàn)一個簡單的構造函數(shù)用來創(chuàng)建對象:
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");
}
現(xiàn)在我們來對比一下兩者調用的區(qū)別:
obj.whoAmI(); // 輸出"MyObj" (和期望一致)
whoAmI(); // 輸出"window" (竟然輸出了window)
當我們把 obj.whoAmI
賦值給 whoAmI
的時候,這個新的變量 whoAmI
是定義在全局下,因此 this
指向全局的 window
,而不是 MyObj
。如果我們真的要獲取對 MyObj
的函數(shù)的引用,需要在其作用域下。
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函數(shù)第一個參數(shù)誤用字符串
如果你將一個字符串作為setTimeout/setTimeInterval,它會被傳給函數(shù)構造函數(shù)并構建一個新的函數(shù)。該操作流程很慢而且低效,并導致bug出現(xiàn)。
var hello = function(){
console.log("hello, fundebug !");
}
setTimeout("hello", 1000);
一個好的替代方法就是傳入函數(shù)作為參數(shù):
setInterval(logTime, 1000); // 將logTime函數(shù)傳入
setTimeout(function() { // 傳入一個匿名函數(shù)
logMessage(msgValue);
}, 1000);
10. 未能成功使用strict mode
使用strict model會增加很多限制條件來加強安全和防止某些錯誤的出現(xiàn),如果不使用 strict mode
,你就相當于少了一個得力的助手幫你避免錯誤:
- 更加容易debug
- 避免不小心定義了不該定義的全局變量
- 避免this隱式轉換
- 避免屬性名字或則參數(shù)值的重復使用
- eval()更加安全
- 無效地使用delete會自動拋出錯誤
版權聲明:
轉載時請注明作者Fundebug以及本文地址:
https://blog.fundebug.com/2017/11/15/top_10_bugs_and_fixing_method/