譯者 | 劉汪洋
審校 | 重樓
概括:這篇文章介紹了 JavaScript 中各種循環(huán)語句的特點和性能,以及如何根據(jù)不同的場景選擇合適的循環(huán)方式。文章通過一些實例和測試,給出了一些使用循環(huán)的最佳實踐,如避免不必要的計算、使用緩存變量、使用 break 和 continue 等。
介紹
循環(huán)是編程語言的基本組成部分,在 JavaScript 中也同樣不可或缺。JavaScript 循環(huán)是一種功能強大的編程工具,它讓開發(fā)者能夠遍歷數(shù)據(jù)集合,針對每個項目執(zhí)行特定操作,并根據(jù)具體條件作出決策。
然而,實現(xiàn)方式不當可能導(dǎo)致負面問題。編寫錯誤的循環(huán)不僅可能引發(fā)性能問題和編程錯誤,還可能使代碼變得難以維護。
無論你是編程新手還是有豐富經(jīng)驗的開發(fā)人員,編寫高效且合理的循環(huán)總是一項充滿挑戰(zhàn)的任務(wù)。
在本全面指南中,我們將詳細探討編寫 JavaScript 循環(huán)的最佳實踐,助你將代碼從新手水平提升至專家水平。我們將從最基本的部分開始,涵蓋如何選擇正確的循環(huán)類型和優(yōu)化循環(huán)性能等方面。
后面,我們還將深入探討更高級的主題,包括如何運用函數(shù)式編程技巧,以及如何在處理異步循環(huán)時保持謹慎,以避免代碼運行時出現(xiàn)不可預(yù)見的情況。
不論你是使用 數(shù)組、對象 還是其他數(shù)據(jù)結(jié)構(gòu),我們所分享的技巧和方法都將有助于你編寫干凈、簡潔且無 bug 的循環(huán)代碼。
因此,如果你準備深入了解 JavaScript 循環(huán)的精髓,那就做好準備,讓我們共同探索吧!
選擇正確類型的循環(huán)
在 JavaScript 編程中,選擇合適的循環(huán)類型很關(guān)鍵。JavaScript 提供了幾種主要的循環(huán)類型,包括 for 循環(huán)、while 循環(huán)和do-while 循環(huán),每種類型都有其適用場景及優(yōu)缺點。
For 循環(huán)
For 循環(huán)是 JavaScript 中最常用的循環(huán)類型,特別適用于在已知循環(huán)次數(shù)的情況。
例如,要計算數(shù)字數(shù)組的和:
let numbers = [1, 2, 3, 4, 5];
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
console.log(sum); // 輸出:15
在此示例中,for 循環(huán)遍歷數(shù)組的每個元素并累加其值。
While 循環(huán)
While 循環(huán)適用于重復(fù)執(zhí)行代碼塊直到滿足某條件的場景。
例如,生成大于 0.5 的隨機數(shù):
let randomNumber = Math.random();
while (randomNumber <= 0.5) {
randomNumber = Math.random();
}
console.log(randomNumber); // 輸出:大于 0.5 的數(shù)字
此例中,while 循環(huán)不斷生成新的隨機數(shù),直到滿足條件。
Do-While 循環(huán)
Do-while 循環(huán)與while 循環(huán)相似,但無論初始條件如何,至少會執(zhí)行一次代碼塊。
例如,要求用戶輸入一個介于 1 和 10 之間的數(shù)字:
let number;
do {
number = prompt('輸入一個介于 1 和 10 之間的數(shù)字:');
} while (number < 1 || number > 10);
console.log(number); // 輸出:介于 1 和 10 之間的數(shù)字
此例中,do-while 循環(huán)反復(fù)提示用戶輸入合法數(shù)字。
選擇合適的循環(huán)類型取決于具體需求和任務(wù)。如果已知執(zhí)行次數(shù),通常選擇 for 循環(huán);如果需要滿足某個條件才停止執(zhí)行,則 while 循環(huán)或 do-while 循環(huán)可能更合適。通過慎重選擇循環(huán)類型,可以使代碼執(zhí)行更高效、更易于理解。
循環(huán)控制語句的正確使用
選擇了合適的循環(huán)結(jié)構(gòu)后,你還需要深入了解如何在 JavaScript 中正確運用兩種主要的循環(huán)控制語句:break 和 continue。
這兩種控制語句可以讓你更靈活地控制循環(huán)的執(zhí)行流程。當滿足特定條件時,break 語句用于完全終止循環(huán),而 continue 語句則用于跳過當前迭代并進入下一個迭代。
例如,設(shè)有一個數(shù)字數(shù)組,你想通過 for 循環(huán)找到數(shù)組中的第一個偶數(shù)數(shù)字。一旦找到第一個偶數(shù),你可以使用 break 語句退出循環(huán):
const numbers = [1, 3, 2, 5, 4, 7, 6];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
console.log(`第一個偶數(shù)是 ${numbers[i]}`);
break;
}
}
在這個例子中,一旦找到第一個偶數(shù)(即 2),循環(huán)就會終止。如果沒有 break 語句,循環(huán)會繼續(xù)遍歷數(shù)組的其余部分。
相對于上述情況,如果你想在數(shù)組中打印所有的奇數(shù),并跳過所有偶數(shù),可以使用 continue 語句:
const numbers = [1, 3, 2, 5, 4, 7, 6];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
continue;
}
console.log(`奇數(shù):${numbers[i]}`);
}
在這個例子中,循環(huán)會跳過偶數(shù),僅打印奇數(shù)(即 1、3、5 和 7)。如果沒有 continue 語句,循環(huán)將會打印數(shù)組中的所有數(shù)字。
如今,我們已經(jīng)掌握了如何為特定任務(wù)選擇適當?shù)难h(huán),以及何時使用 break 和 continue 語句。
下一步我們將探討的是編寫循環(huán)時需考慮的另一個重要方面:優(yōu)化循環(huán)性能。
優(yōu)化循環(huán)的性能
在日常編碼過程中,循環(huán)性能的優(yōu)化可能不會引起太多關(guān)注。但是當面臨大數(shù)據(jù)處理時,循環(huán)性能的提高就成為了關(guān)鍵任務(wù),因為它會對整個 JavaScript 代碼的性能產(chǎn)生決定性影響。因此,了解如何分析和增強循環(huán)性能變得至關(guān)重要。
以下是一些關(guān)于如何優(yōu)化循環(huán)性能的實用建議:
避免不必要的計算
一個提高循環(huán)性能的常見方法是減少不必要的計算。為了讓這一點更加明確,我們可以參考下述示例:
假設(shè)你正在對一個名為 array 的數(shù)組進行迭代。在這種場景下,如果你使用 for 循環(huán)遍歷數(shù)組,每一次的迭代都會重新計算數(shù)組的 length 以確定是否應(yīng)繼續(xù)循環(huán)。
以下列代碼為例:
let arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
在每次迭代中,系統(tǒng)都會檢測 i 是否小于數(shù)組長度。但如果數(shù)組非常龐大,每次重新獲取長度可能會成為性能瓶頸。注意:JavaScript 中的數(shù)組是特殊的對象,能夠容納各種數(shù)據(jù)類型。每個數(shù)組都帶有一個 length 屬性,反映了其元素數(shù)量,且會隨著元素的增減自動更新。
為了改善性能,你可以在循環(huán)外部將數(shù)組的 length 存儲為一個變量:
let arr = [1, 2, 3, 4, 5];
let len = arr.length; // 緩存長度
for (let i = 0; i < len; i++) {
console.log(arr[i]);
}
這樣做可以避免在每次迭代中都訪問數(shù)組的 length 屬性,從而顯著提高代碼的運行效率。對于處理大型數(shù)組,這個技巧可能特別有效。
注意:現(xiàn)代 JavaScript 引擎(如 V8)已經(jīng)高度優(yōu)化了循環(huán)。因此,有時緩存數(shù)組的長度可能不會顯著提升性能。不過,作為良好編程實踐,特別是在處理大型數(shù)組或性能敏感代碼時,這種技巧依然有價值。
高效循環(huán)的應(yīng)用
采用高效的循環(huán)結(jié)構(gòu)來提升循環(huán)性能是一項重要手段。
例如,在數(shù)組遍歷過程中,for 循環(huán)通常具有比 forEach 循環(huán)更高的執(zhí)行效率。
以下代碼片段可以讓我們深入理解這一觀點:
let arr = [1, 2, 3, 4, 5]; // 使用 for 循環(huán)遍歷數(shù)組
for (let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i]);
}
在此例中,我們采用 for 循環(huán)來遍歷 arr 數(shù)組,但同樣的效果可以通過 forEach 循環(huán)實現(xiàn)。
那么,for 循環(huán)為何具有更高的執(zhí)行效率呢?
這個問題較為復(fù)雜,有許多細節(jié)值得深入探討。其中一個關(guān)鍵原因是,forEach 循環(huán)會為數(shù)組的每個元素創(chuàng)建一個新的函數(shù)作用域。
換言之,每個元素都會生成一個新的函數(shù),并在迭代結(jié)束后將其銷毀。
對于小型數(shù)組,這一過程影響不大。但是,當處理大型數(shù)組時,它可能會對循環(huán)的執(zhí)行速度產(chǎn)生負面影響。
與之相對,for 循環(huán)只為整個循環(huán)過程創(chuàng)建一次函數(shù)作用域,因此僅需要創(chuàng)建并銷毀一個函數(shù)作用域。
另外,for 循環(huán)還支持使用 break 和 continue 來靈活控制循環(huán)的迭代過程,而 forEach 循環(huán)則無法實現(xiàn)這一功能。
正因如此,for 循環(huán)在數(shù)組遍歷時具備更高的優(yōu)化潛力。
DOM 操作的最小化
在循環(huán)過程中,減少對文檔對象模型(DOM)的操作至關(guān)重要,因為在 JavaScript 中,與 DOM 的交互通常是一個耗時的過程。
每一次更改 DOM 都會讓瀏覽器重新布局和繪制頁面,可能降低應(yīng)用程序的執(zhí)行速度,特別是處理大量元素時。
例如:
// 低效的 DOM 操作
for (let i = 0; i < 1000; i++) {
document.getElementById('myDiv').innerHTML += '<p>' + i + '</p>';
}
// 更高效的 DOM 操作
let output = '';
for (let i = 0; i < 1000; i++) {
output += '<p>' + i + '</p>';
}
document.getElementById('myDiv').innerHTML = output;
在上述例子中,第一個循環(huán)效率較低,因為每次迭代都直接操作 DOM。每次迭代都會給 myDiv 元素的 innerHTML 屬性添加新段落,導(dǎo)致了 1000 次 DOM 操作。
與此相反,第二個循環(huán)的效率更高,原因在于它減少了對 DOM 的直接操作。循環(huán)并不是在每次迭代時都更新 DOM,而是將 HTML 字符串累加到名為 output 的變量中。一旦循環(huán)完成,就使用 output 字符串僅一次更新 myDiv 元素的 innerHTML 屬性,從而實現(xiàn)了只進行一次 DOM 操作,大大提高了效率。
規(guī)避常見錯誤
在使用 JavaScript 編程時,開發(fā)人員可能會遇到一些典型的錯誤。以下要探討的這些錯誤在實際編程中應(yīng)引起警覺,并積極規(guī)避。
循環(huán)內(nèi)部改動循環(huán)變量
在循環(huán)內(nèi)部更改循環(huán)變量可能引發(fā)預(yù)期不到的效果或錯誤。此做法通常并不合適。理想的方案是使用一個單獨的變量來存儲你希望更改的值。
例如,如下循環(huán):
for (let i = 0; i < 10; i++) {
console.log(i); i++; // 在循環(huán)內(nèi)修改 i
}
上述代碼中,i 在循環(huán)中被修改,可能造成不在期望范圍的結(jié)果。相對地,你應(yīng)該像下面這樣使用一個單獨的變量來存儲你想要修改的值:
for (let i = 0; i < 10; i++) {
console.log(i);
let modifiedValue = i + 1; // 使用一個單獨的變量來修改 i
}
忽略大括號的使用
雖然你可以在編寫循環(huán)時省略大括號,但這通常并不推薦。缺少大括號可能復(fù)雜化代碼的閱讀和理解,甚至可能引發(fā)錯誤或不預(yù)期的效果。
例如,如下循環(huán):
for (let i = 0; i < 10; i++)
console.log(i);
由于此循環(huán)未使用大括號,可能使代碼的閱讀和理解變得困難,因此即使是單行語句,也最好始終使用大括號:
for (let i = 0; i < 10; i++) {
console.log(i);
}
避免無限循環(huán)
無限循環(huán)指的是永不終止的循環(huán)。當退出條件設(shè)置不當,或永不滿足時,就可能出現(xiàn)這種情況。無限循環(huán)不僅可能導(dǎo)致程序崩潰或掛起,還可能非常難以調(diào)試。
例如:
觀察下面的循環(huán),它沒有增加變量值,也未檢查是否超過某個閾值:
let count = 0;
while (count < 10) {
console.log(count);
}
由于 count 變量未曾增加,此代碼將觸發(fā)無限循環(huán)。
因此,你應(yīng)該時刻警惕無限循環(huán)的可能性。為了避免無限循環(huán),請確保設(shè)置了正確的退出條件,并對代碼進行了充分測試。
“越界”錯誤
在編程中,越界錯誤是一種常見問題,發(fā)生在使用不正確的比較運算符或未充分考慮循環(huán)變量初始值的情況下。這些錯誤可能導(dǎo)致程序結(jié)果不準確,甚至在某些情況下會觸發(fā)程序崩潰。
例如,假設(shè)你有一個包含 10 個元素的數(shù)組,你計劃遍歷它,并對每個元素執(zhí)行特定操作。
正確的循環(huán)編寫如下:
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
該代碼將遍歷數(shù)組的全部 10 個元素,并將各個元素的值輸出到控制臺。但是,如果你誤用了比較運算符,將 i< arr.length寫成 i<=arr.length會怎樣呢?
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let i = 0; i <= arr.length; i++) {
console.log(arr[i]);
}
這段代碼將觸發(fā)一個越界錯誤,因為當i等于 arr.length時,將嘗試訪問一個不存在的元素。循環(huán)將執(zhí)行共計11 次而非 10 次,最后一次迭代將拋出錯誤,因為 arr[10] 的值為 undefined。為了防止越界錯誤的發(fā)生,你應(yīng)仔細檢查循環(huán)條件、確保使用正確的比較運算符,并充分考慮循環(huán)變量的初始值。通過避免這些常見錯誤,你能夠確保循環(huán)的準確執(zhí)行,從而降低程序中的錯誤或崩潰風險。同時,在執(zhí)行代碼之前,仔細審查潛在錯誤始終是明智的做法。
在適當場合,選擇遞歸
在程序設(shè)計中,遞歸經(jīng)常被視為一種優(yōu)雅的解決方案。
遞歸特別適用于處理層級數(shù)量未知或不固定的分層數(shù)據(jù)結(jié)構(gòu),如樹形結(jié)構(gòu)或嵌套數(shù)組。
與傳統(tǒng)的嵌套循環(huán)相比,當處理具有未知深度的結(jié)構(gòu)時,遞歸能使代碼更整潔、易讀。
警告:使用遞歸時需謹慎,確保遞歸有明確的終止條件,避免發(fā)生無限遞歸和棧溢出錯誤。
例如,考慮以下情景:
假設(shè)你擁有一個包含嵌套數(shù)字的數(shù)組結(jié)構(gòu),你的任務(wù)是找到數(shù)組中所有數(shù)字(包括嵌套的數(shù)字)的總和。在此情景下,遞歸可以被優(yōu)雅地應(yīng)用。下面是一個通過遞歸來求嵌套數(shù)組總和的示例代碼:
const nestedArray = [[1, 2, [3]], 4];
function sumArray(arr) {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
// 如果當前元素是數(shù)組,則遞歸處理
sum += sumArray(arr[i]);
} else {
// 如果當前元素是數(shù)字,則加入總和
sum += arr[i];
}
}
return sum;
}
console.log(sumArray(nestedArray)); // 輸出:10
此代碼利用遞歸深入遍歷嵌套數(shù)組 nestedArray。它可以計算所有數(shù)字的總和,而不受嵌套深度的限制。
遞歸函數(shù)允許自身反復(fù)調(diào)用,直到達到最底層的嵌套結(jié)構(gòu),并返回一個可用于計算最終總和的值。
通過檢查每個元素是否為數(shù)組,該函數(shù)遞歸處理數(shù)組元素,直到達到最深的嵌套層級,即數(shù)字。
然后,函數(shù)將該數(shù)字返回到上一遞歸層。這樣便于計算當前層級所有元素的總和。
通過這種方式,遞歸提供了一種靈活而優(yōu)雅的方法,用于處理未知或不固定層級結(jié)構(gòu)的問題。
結(jié)論
JavaScript 循環(huán)非常重要,但要精通使用它可能有一定的難度。遵循以上最佳實踐,你將能夠編寫更高效、更合理的循環(huán)代碼,進而提升整體的編程水準。
要選擇合適的循環(huán)類型,并準確地使用循環(huán)控制語句來優(yōu)化循環(huán)性能,避免常見的錯誤。同時,在適宜的場景下,你可以考慮遞歸的使用。只要掌握這些技巧,你就能大幅增強自己在 JavaScript 循環(huán)方面的專業(yè)技能。
因此,如果你投入時間來評估和優(yōu)化循環(huán)代碼,你的代碼性能將得到顯著提升。
譯者介紹
劉汪洋,51CTO社區(qū)編輯,昵稱:明明如月,一個擁有 5 年開發(fā)經(jīng)驗的某大廠高級 Java 工程師,擁有多個主流技術(shù)博客平臺博客專家稱號。
原文標題:JavaScript Loop: The Best Practices to Have the Optimal Performance,作者:robiulhr