錯誤的 JavaScript 代碼:JavaScript 開發人員最常犯的 10 個錯誤
已發表: 2022-03-11今天,JavaScript 是幾乎所有現代 Web 應用程序的核心。 特別是在過去的幾年中,我們見證了各種強大的基於 JavaScript 的庫和框架的激增,這些庫和框架用於單頁應用程序 (SPA) 開發、圖形和動畫,甚至是服務器端 JavaScript 平台。 JavaScript 已經真正在 Web 應用程序開發領域變得無處不在,因此掌握的技能越來越重要。
乍一看,JavaScript 可能看起來很簡單。 事實上,將基本的 JavaScript 功能構建到網頁中對於任何有經驗的軟件開發人員來說都是一項相當簡單的任務,即使他們是 JavaScript 新手。 然而,這種語言比人們最初認為的要微妙、強大和復雜得多。 事實上,JavaScript 的許多細微之處都會導致一些常見問題,使其無法正常工作——我們在此討論其中的 10 個——在成為 JavaScript 開發大師的過程中,了解和避免這些問題很重要。
常見錯誤 #1:不正確this
引用
我曾經聽一位喜劇演員說:
我不是真的在這裡,因為除了“t”之外,這裡還有什麼?
這個笑話在很多方面都體現了開發人員對 JavaScript 的this
關鍵字經常存在的混淆類型。 我的意思是, this
真的是這樣,還是完全不同? 還是未定義?
隨著這些年來 JavaScript 編碼技術和設計模式變得越來越複雜,回調和閉包中自引用範圍的擴散也相應增加,這是“這個/那個混淆”的一個相當常見的來源。
考慮這個示例代碼片段:
Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };
執行上述代碼會導致以下錯誤:
Uncaught TypeError: undefined is not a function
為什麼?
這都是關於上下文的。 出現上述錯誤的原因是,當您調用setTimeout()
時,實際上是在調用window.setTimeout()
。 因此,傳遞給setTimeout()
的匿名函數是在window
對象的上下文中定義的,該對像沒有clearBoard()
方法。
一個傳統的、與舊瀏覽器兼容的解決方案是簡單地將您對this
的引用保存在一個變量中,然後該變量可以被閉包繼承; 例如:
Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 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(); // ahhh, back in the context of the right 'this'! };
常見錯誤 #2:認為存在塊級範圍
正如我們的 JavaScript 招聘指南中所討論的,JavaScript 開發人員之間的一個常見混淆來源(因此也是一個常見的錯誤來源)是假設 JavaScript 為每個代碼塊創建了一個新的範圍。 儘管在許多其他語言中都是如此,但在 JavaScript 中卻不是這樣。 例如,考慮以下代碼:
for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?
如果您猜測console.log()
調用將輸出undefined
或拋出錯誤,那麼您猜錯了。 信不信由你,它將輸出10
。 為什麼?
在大多數其他語言中,上面的代碼會導致錯誤,因為變量i
的“生命”(即範圍)將被限制在for
塊中。 然而,在 JavaScript 中,情況並非如此,即使在for
循環完成後變量i
仍保留在範圍內,並在退出循環後保留其最後一個值。 (順便說一下,這種行為被稱為變量提升)。
不過值得注意的是,對塊級作用域的支持正在通過新的let
關鍵字進入 JavaScript。 let
關鍵字已經在 JavaScript 1.7 中可用,並且計劃從 ECMAScript 6 開始成為官方支持的 JavaScript 關鍵字。
JavaScript 新手? 閱讀範圍、原型等。
常見錯誤 #3:造成內存洩漏
如果您沒有有意識地編寫代碼來避免它們,那麼內存洩漏幾乎是不可避免的 JavaScript 問題。 它們的發生方式有很多種,因此我們將重點介紹其中一些更常見的情況。
內存洩漏示例 1:對已失效對象的懸空引用
考慮以下代碼:
var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second
如果你運行上面的代碼並監控內存使用情況,你會發現你有一個巨大的內存洩漏,每秒洩漏一兆字節! 甚至手動 GC 也無濟於事。 所以看起來我們每次調用replaceThing
時都會洩漏longStr
。 但為什麼?
讓我們更詳細地研究一下:
每個theThing
像都包含自己的 1MB longStr
對象。 每一秒,當我們調用replaceThing
時,它都會保留對priorThing 中先前theThing
對象的priorThing
。 但是我們仍然認為這不會是一個問題,因為每次通過之前,先前引用的priorThing
都會被取消引用(當通過priorThing
priorThing = theThing;
重置priorThing 時)。 而且,僅在replaceThing
的主體和unused
的函數中引用,實際上從未使用過。
所以我們再次想知道為什麼這裡有內存洩漏!?
要了解發生了什麼,我們需要更好地了解 JavaScript 在底層是如何工作的。 實現閉包的典型方式是每個函數對像都有一個指向表示其詞法範圍的字典式對象的鏈接。 如果在replaceThing
中定義的兩個函數實際上都使用priorThing
,那麼即使priorThing
被一遍又一遍地分配,它們都獲得相同的對像很重要,因此兩個函數共享相同的詞法環境。 但是,一旦任何閉包使用了一個變量,它就會最終進入該範圍內所有閉包共享的詞法環境中。 正是這種細微的差別導致了這種粗糙的內存洩漏。 (有關這方面的更多詳細信息,請點擊此處。)
內存洩漏示例 2:循環引用
考慮這個代碼片段:
function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }
在這裡, onClick
有一個閉包,它保持對element
的引用(通過element.nodeName
)。 通過將onClick
也分配給element.click
,循環引用被創建; 即: element
-> onClick
-> element
-> onClick
-> element
…
有趣的是,即使從 DOM 中刪除element
,上面的循環自引用也會阻止element
和onClick
被收集,從而導致內存洩漏。
避免內存洩漏:你需要知道的
JavaScript 的內存管理(特別是垃圾回收)主要基於對象可達性的概念。
假設以下對像是可訪問的並稱為“根”:
- 從當前調用堆棧中的任何位置引用的對象(即當前正在調用的函數中的所有局部變量和參數,以及閉包範圍內的所有變量)
- 所有全局變量
至少只要可以通過引用或引用鏈從任何根訪問對象,它們就會保存在內存中。
瀏覽器中有一個垃圾收集器(GC),用於清理不可達對象佔用的內存; 即,當且僅當GC 認為對像不可訪問時,對象才會從內存中刪除。 不幸的是,很容易得到實際上不再使用但 GC 仍然認為是“可訪問”的已失效的“殭屍”對象。
常見錯誤#4:對平等的混淆
JavaScript 的便利之一是它會自動將布爾上下文中引用的任何值強制轉換為布爾值。 但是在某些情況下,這可能會讓人感到困惑,因為它很方便。 例如,已知以下一些內容會困擾許多 JavaScript 開發人員:
// All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...
關於最後兩個,儘管是空的(這可能會導致人們相信它們會評估為false
),但{}
和[]
實際上都是對象,並且任何對像都將被強制轉換為 JavaScript 中的布爾值true
,符合 ECMA-262 規範。
正如這些示例所展示的,類型強制轉換的規則有時會一清二楚。 因此,除非明確需要類型強制,否則通常最好使用===
和!==
(而不是==
和!=
),以避免類型強制的任何意外副作用。 ( ==
和!=
在比較兩個事物時會自動執行類型轉換,而===
和!==
在不進行類型轉換的情況下執行相同的比較。)
完全作為一個側面——但由於我們正在談論類型強制和比較——值得一提的是,將NaN
與任何東西(甚至是NaN
!)進行比較總是會返回false
。 因此,您不能使用相等運算符( ==
、 ===
、 !=
、 !==
)來確定值是否為NaN
。 相反,使用內置的全局isNaN()
函數:
console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true
常見錯誤 #5:低效的 DOM 操作
JavaScript 使操作 DOM 變得相對容易(即添加、修改和刪除元素),但沒有做任何事情來提高這樣做的效率。
一個常見的示例是一次添加一系列 DOM 元素的代碼。 添加 DOM 元素是一項昂貴的操作。 連續添加多個 DOM 元素的代碼效率低下並且可能無法正常工作。
當需要添加多個 DOM 元素時,一種有效的替代方法是改用文檔片段,從而提高效率和性能。
例如:
var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));
除了這種方法本身提高了效率之外,創建附加的 DOM 元素的成本很高,而在分離時創建和修改它們然後附加它們會產生更好的性能。

常見錯誤 #6:在for
循環中不正確地使用函數定義
考慮這段代碼:
var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }
根據上面的代碼,如果有 10 個輸入元素,點擊其中任何一個都會顯示“This is element #10”! 這是因為,當對任何元素調用onclick
時,上述 for 循環已經完成,並且i
的值已經是 10(對於所有元素)。
但是,我們可以通過以下方式糾正上述代碼問題以實現所需的行為:
var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }
在這個修改後的代碼版本中,每次我們通過循環時都會立即執行makeHandler
,每次接收i+1
的當前值並將其綁定到一個作用域num
變量。 外部函數返回內部函數(它也使用這個作用域的num
變量)並且元素的onclick
設置為該內部函數。 這確保了每個onclick
接收並使用正確的i
值(通過作用域num
變量)。
常見錯誤 #7:未能正確利用原型繼承
驚人的高比例的 JavaScript 開發人員未能完全理解,因此無法充分利用原型繼承的特性。
這是一個簡單的例子。 考慮這段代碼:
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); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'
但是如果我們這樣做呢:
delete secondObj.name;
然後我們會得到:
console.log(secondObj.name); // -> Results in 'undefined'
但是,將其恢復為“默認”不是更好嗎? 如果我們修改原始代碼以利用原型繼承,這很容易做到,如下所示:
BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';
在這個版本中, BaseObject
繼承了它的prototype
對象的name
屬性,它被設置(默認)為'default'
。 因此,如果在沒有名稱的情況下調用構造函數,則名稱將默認為default
。 同樣,如果從BaseObject
的實例中刪除name
屬性,則將搜索原型鏈並從其值仍為'default'
的prototype
對像中檢索name
屬性。 所以現在我們得到:
var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'
常見錯誤 #8:創建對實例方法的錯誤引用
讓我們定義一個簡單的對象,並創建它並實例化它,如下所示:
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();
現在,為方便起見,讓我們創建對whoAmI
方法的引用,大概這樣我們就可以僅通過whoAmI()
而不是更長的obj.whoAmI()
來訪問它:
var whoAmI = obj.whoAmI;
並且為了確保一切看起來都很順從,讓我們打印出我們的新whoAmI
變量的值:
console.log(whoAmI);
輸出:
function () { console.log(this === window ? "window" : "MyObj"); }
嗯不錯。 看起來不錯。
但是現在,看看我們調用obj.whoAmI()
與我們方便的引用whoAmI()
時的區別:
obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)
什麼地方出了錯?
這裡的假貨是,當我們進行賦值時var whoAmI = obj.whoAmI;
,新變量whoAmI
被定義在全局命名空間中。 結果,它的this
值是window
,而不是MyObject
的obj
實例!
因此,如果我們真的需要創建對對象現有方法的引用,我們需要確保在該對象的命名空間內進行,以保留this
的值。 這樣做的一種方法是,例如,如下:
var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)
常見錯誤 #9:提供一個字符串作為setTimeout
或setInterval
的第一個參數
首先,讓我們在這裡明確一點:提供一個字符串作為setTimeout
或setInterval
的第一個參數本身並不是一個錯誤。 它是完全合法的 JavaScript 代碼。 這裡的問題更多是性能和效率。 很少有人解釋的是,在後台,如果您將字符串作為第一個參數傳遞給setTimeout
或setInterval
,它將被傳遞給函數構造函數以轉換為新函數。 此過程可能緩慢且效率低下,並且很少需要。
將字符串作為第一個參數傳遞給這些方法的替代方法是傳入一個函數。 讓我們看一個例子。
那麼,這裡將是setInterval
和setTimeout
的一個相當典型的用法,將字符串作為第一個參數傳遞:
setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);
更好的選擇是傳入一個函數作為初始參數; 例如:
setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);
常見錯誤 #10:未能使用“嚴格模式”
正如我們的 JavaScript 招聘指南中所解釋的,“嚴格模式”(即,包括'use strict';
在 JavaScript 源文件的開頭)是一種在運行時自願對 JavaScript 代碼強制執行更嚴格的解析和錯誤處理的方法,以及使其更安全。
誠然,不使用嚴格模式本身並不是一個“錯誤”,但它的使用越來越受到鼓勵,而省略它也越來越被認為是不好的形式。
以下是嚴格模式的一些主要好處:
- 使調試更容易。 原本會被忽略或默默失敗的代碼錯誤現在會生成錯誤或引發異常,從而更快地提醒您代碼中的問題並更快地引導您找到它們的源頭。
- 防止意外的全局變量。 如果沒有嚴格模式,為未聲明的變量賦值會自動創建一個具有該名稱的全局變量。 這是 JavaScript 中最常見的錯誤之一。 在嚴格模式下,嘗試這樣做會引發錯誤。
- 消除
this
威壓。 如果沒有嚴格模式,對 null 或 undefined 的this
值的引用會自動強製到全局。 這可能會導致許多假頭和拔出你的頭髮的錯誤。 在嚴格模式下,引用 aathis
null 或 undefined 值會引發錯誤。 - 不允許重複的屬性名稱或參數值。 嚴格模式在檢測到對像中重複的命名屬性(例如,
var object = {foo: "bar", foo: "baz"};
)或函數的重複命名參數(例如,function foo(val1, val2, val1){}
),從而捕獲幾乎可以肯定是代碼中的錯誤,否則您可能會浪費大量時間來跟踪。 - 使 eval() 更安全。
eval()
在嚴格模式和非嚴格模式下的行為方式存在一些差異。 最重要的是,在嚴格模式下,在eval()
語句中聲明的變量和函數不會在包含範圍內創建(它們是在非嚴格模式下在包含範圍內創建的,這也是常見的問題來源)。 - 對
delete
的無效使用引發錯誤。delete
運算符(用於從對像中刪除屬性)不能用於對象的不可配置屬性。 當嘗試刪除不可配置的屬性時,非嚴格代碼會靜默失敗,而嚴格模式會在這種情況下拋出錯誤。
包起來
與任何技術一樣,您越了解 JavaScript 工作和不工作的原因和方式,您的代碼就越可靠,您就越能夠有效地利用該語言的真正力量。 相反,缺乏對 JavaScript 範式和概念的正確理解確實是許多 JavaScript 問題所在。
徹底熟悉語言的細微差別是提高熟練度和提高生產力的最有效策略。 當您的 JavaScript 不工作時,避免許多常見的 JavaScript 錯誤會有所幫助。