AI 日报

JavaScript 循环:拥有最佳性能的最佳实践

  • By admin
  • Sep 19, 2023 - 2 min read



译者 | 刘汪洋

审校 | 重楼

概括:这篇文章介绍了 JavaScript 中各种循环语句的特点和性能,以及如何根据不同的场景选择合适的循环方式。文章通过一些实例和测试,给出了一些使用循环的最佳实践,如避免不必要的计算、使用缓存变量、使用 break 和 continue 等。

介绍

循环是编程语言的基本组成部分,在 JavaScript 中也同样不可或缺。JavaScript 循环是一种功能强大的编程工具,它让开发者能够遍历数据集合,针对每个项目执行特定操作,并根据具体条件作出决策。

然而,实现方式不当可能导致负面问题。编写错误的循环不仅可能引发性能问题和编程错误,还可能使代码变得难以维护。
无论你是编程新手还是有丰富经验的开发人员,编写高效且合理的循环总是一项充满挑战的任务。

在本全面指南中,我们将详细探讨编写 JavaScript 循环的最佳实践,助你将代码从新手水平提升至专家水平。我们将从最基本的部分开始,涵盖如何选择正确的循环类型和优化循环性能等方面。
后面,我们还将深入探讨更高级的主题,包括如何运用函数式编程技巧,以及如何在处理异步循环时保持谨慎,以避免代码运行时出现不可预见的情况。

不论你是使用 数组对象 还是其他数据结构,我们所分享的技巧和方法都将有助于你编写干净、简洁且无 bug 的循环代码。
因此,如果你准备深入了解 JavaScript 循环的精髓,那就做好准备,让我们共同探索吧!

选择正确类型的循环

在 JavaScript 编程中,选择合适的循环类型很关键。JavaScript 提供了几种主要的循环类型,包括 for 循环while 循环do-while 循环,每种类型都有其适用场景及优缺点。

For 循环

For 循环是 JavaScript 中最常用的循环类型,特别适用于在已知循环次数的情况。
例如,要计算数字数组的和:

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 循环遍历数组的每个元素并累加其值。

While 循环

While 循环适用于重复执行代码块直到满足某条件的场景。
例如,生成大于 0.5 的随机数:

let randomNumber = Math.random();
while (randomNumber <= 0.5) {
  randomNumber = Math.random();
}
console.log(randomNumber); // 输出:大于 0.5 的数字

此例中,while 循环不断生成新的随机数,直到满足条件。

Do-While 循环

Do-while 循环与while 循环相似,但无论初始条件如何,至少会执行一次代码块。
例如,要求用户输入一个介于 1 和 10 之间的数字:

let number;
do {
  number = prompt('输入一个介于 1 和 10 之间的数字:');
} while (number < 1 || number > 10);
console.log(number); // 输出:介于 1 和 10 之间的数字

此例中,do-while 循环反复提示用户输入合法数字。
选择合适的循环类型取决于具体需求和任务。如果已知执行次数,通常选择 for 循环;如果需要满足某个条件才停止执行,则 while 循环或 do-while 循环可能更合适。通过慎重选择循环类型,可以使代码执行更高效、更易于理解。

循环控制语句的正确使用

选择了合适的循环结构后,你还需要深入了解如何在 JavaScript 中正确运用两种主要的循环控制语句:break 和 continue。
这两种控制语句可以让你更灵活地控制循环的执行流程。当满足特定条件时,break 语句用于完全终止循环,而 continue 语句则用于跳过当前迭代并进入下一个迭代。
例如,设有一个数字数组,你想通过 for 循环找到数组中的第一个偶数数字。一旦找到第一个偶数,你可以使用 break 语句退出循环:

const numbers = [1, 3, 2, 5, 4, 7, 6];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    console.log(`第一个偶数是 ${numbers[i]}`);
    break;
  }
}

在这个例子中,一旦找到第一个偶数(即 2),循环就会终止。如果没有 break 语句,循环会继续遍历数组的其余部分。
相对于上述情况,如果你想在数组中打印所有的奇数,并跳过所有偶数,可以使用 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(`奇数:${numbers[i]}`);
}

在这个例子中,循环会跳过偶数,仅打印奇数(即 1、3、5 和 7)。如果没有 continue 语句,循环将会打印数组中的所有数字。
如今,我们已经掌握了如何为特定任务选择适当的循环,以及何时使用 break 和 continue 语句。
下一步我们将探讨的是编写循环时需考虑的另一个重要方面:优化循环性能。

优化循环的性能

在日常编码过程中,循环性能的优化可能不会引起太多关注。但是当面临大数据处理时,循环性能的提高就成为了关键任务,因为它会对整个 JavaScript 代码的性能产生决定性影响。因此,了解如何分析和增强循环性能变得至关重要。
以下是一些关于如何优化循环性能的实用建议:

避免不必要的计算

一个提高循环性能的常见方法是减少不必要的计算。为了让这一点更加明确,我们可以参考下述示例:
假设你正在对一个名为 array 的数组进行迭代。在这种场景下,如果你使用 for 循环遍历数组,每一次的迭代都会重新计算数组的 length 以确定是否应继续循环。
以下列代码为例:

let arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) { 
  console.log(arr[i]); 
}

在每次迭代中,系统都会检测 i 是否小于数组长度。但如果数组非常庞大,每次重新获取长度可能会成为性能瓶颈。注意:JavaScript 中的数组是特殊的对象,能够容纳各种数据类型。每个数组都带有一个 length 属性,反映了其元素数量,且会随着元素的增减自动更新。

为了改善性能,你可以在循环外部将数组的 length 存储为一个变量:

let arr = [1, 2, 3, 4, 5];
let len = arr.length; // 缓存长度
for (let i = 0; i < len; i++) { 
  console.log(arr[i]); 
}

这样做可以避免在每次迭代中都访问数组的 length 属性,从而显著提高代码的运行效率。对于处理大型数组,这个技巧可能特别有效。
注意:现代 JavaScript 引擎(如 V8)已经高度优化了循环。因此,有时缓存数组的长度可能不会显著提升性能。不过,作为良好编程实践,特别是在处理大型数组或性能敏感代码时,这种技巧依然有价值。

高效循环的应用

采用高效的循环结构来提升循环性能是一项重要手段。
例如,在数组遍历过程中,for 循环通常具有比 forEach 循环更高的执行效率。
以下代码片段可以让我们深入理解这一观点:

let arr = [1, 2, 3, 4, 5]; // 使用 for 循环遍历数组
for (let i = 0, len = arr.length; i < len; i++) { 
  console.log(arr[i]); 
}

在此例中,我们采用 for 循环来遍历 arr 数组,但同样的效果可以通过 forEach 循环实现。

那么,for 循环为何具有更高的执行效率呢?

这个问题较为复杂,有许多细节值得深入探讨。其中一个关键原因是,forEach 循环会为数组的每个元素创建一个新的函数作用域。

换言之,每个元素都会生成一个新的函数,并在迭代结束后将其销毁。

对于小型数组,这一过程影响不大。但是,当处理大型数组时,它可能会对循环的执行速度产生负面影响。

与之相对,for 循环只为整个循环过程创建一次函数作用域,因此仅需要创建并销毁一个函数作用域。

另外,for 循环还支持使用 break 和 continue 来灵活控制循环的迭代过程,而 forEach 循环则无法实现这一功能。

正因如此,for 循环在数组遍历时具备更高的优化潜力。

DOM 操作的最小化

在循环过程中,减少对文档对象模型(DOM)的操作至关重要,因为在 JavaScript 中,与 DOM 的交互通常是一个耗时的过程。
每一次更改 DOM 都会让浏览器重新布局和绘制页面,可能降低应用程序的执行速度,特别是处理大量元素时。
例如:

// 低效的 DOM 操作 
for (let i = 0; i < 1000; i++) { 
  document.getElementById('myDiv').innerHTML += '

' + i + '

'; } // 更高效的 DOM 操作 let output = ''; for (let i = 0; i < 1000; i++) { output += '

' + i + '

'; } document.getElementById('myDiv').innerHTML = output;

在上述例子中,第一个循环效率较低,因为每次迭代都直接操作 DOM。每次迭代都会给 myDiv 元素的 innerHTML 属性添加新段落,导致了 1000 次 DOM 操作。

与此相反,第二个循环的效率更高,原因在于它减少了对 DOM 的直接操作。循环并不是在每次迭代时都更新 DOM,而是将 HTML 字符串累加到名为 output 的变量中。一旦循环完成,就使用 output 字符串仅一次更新 myDiv 元素的 innerHTML 属性,从而实现了只进行一次 DOM 操作,大大提高了效率。

规避常见错误

在使用 JavaScript 编程时,开发人员可能会遇到一些典型的错误。以下要探讨的这些错误在实际编程中应引起警觉,并积极规避。

循环内部改动循环变量

在循环内部更改循环变量可能引发预期不到的效果或错误。此做法通常并不合适。理想的方案是使用一个单独的变量来存储你希望更改的值。
例如,如下循环:

for (let i = 0; i < 10; i++) { 
  console.log(i); i++; // 在循环内修改 i 
}

上述代码中,i 在循环中被修改,可能造成不在期望范围的结果。相对地,你应该像下面这样使用一个单独的变量来存储你想要修改的值:

for (let i = 0; i < 10; i++) { 
  console.log(i); 
  let modifiedValue = i + 1; // 使用一个单独的变量来修改 i 
}

忽略大括号的使用

虽然你可以在编写循环时省略大括号,但这通常并不推荐。缺少大括号可能复杂化代码的阅读和理解,甚至可能引发错误或不预期的效果。
例如,如下循环:

for (let i = 0; i < 10; i++) 
  console.log(i);

由于此循环未使用大括号,可能使代码的阅读和理解变得困难,因此即使是单行语句,也最好始终使用大括号:

for (let i = 0; i < 10; i++) { 
  console.log(i); 
}

避免无限循环

无限循环指的是永不终止的循环。当退出条件设置不当,或永不满足时,就可能出现这种情况。无限循环不仅可能导致程序崩溃或挂起,还可能非常难以调试。
例如:
观察下面的循环,它没有增加变量值,也未检查是否超过某个阈值:

let count = 0; 
while (count < 10) { 
  console.log(count); 
}

由于 count 变量未曾增加,此代码将触发无限循环。
因此,你应该时刻警惕无限循环的可能性。为了避免无限循环,请确保设置了正确的退出条件,并对代码进行了充分测试。

“越界”错误

在编程中,越界错误是一种常见问题,发生在使用不正确的比较运算符或未充分考虑循环变量初始值的情况下。这些错误可能导致程序结果不准确,甚至在某些情况下会触发程序崩溃。
例如,假设你有一个包含 10 个元素的数组,你计划遍历它,并对每个元素执行特定操作。
正确的循环编写如下:

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 
for (let i = 0; i < arr.length; i++) { 
  console.log(arr[i]); 
}

该代码将遍历数组的全部 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]); 
}

这段代码将触发一个越界错误,因为当i等于 arr.length时,将尝试访问一个不存在的元素。循环将执行共计11 次而非 10 次,最后一次迭代将抛出错误,因为 arr[10] 的值为 undefined。为了防止越界错误的发生,你应仔细检查循环条件、确保使用正确的比较运算符,并充分考虑循环变量的初始值。通过避免这些常见错误,你能够确保循环的准确执行,从而降低程序中的错误或崩溃风险。同时,在执行代码之前,仔细审查潜在错误始终是明智的做法。

在适当场合,选择递归

在程序设计中,递归经常被视为一种优雅的解决方案。
递归特别适用于处理层级数量未知或不固定的分层数据结构,如树形结构或嵌套数组。
与传统的嵌套循环相比,当处理具有未知深度的结构时,递归能使代码更整洁、易读。
警告:使用递归时需谨慎,确保递归有明确的终止条件,避免发生无限递归和栈溢出错误。
例如,考虑以下情景:
假设你拥有一个包含嵌套数字的数组结构,你的任务是找到数组中所有数字(包括嵌套的数字)的总和。在此情景下,递归可以被优雅地应用。下面是一个通过递归来求嵌套数组总和的示例代码:

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])) { 
      // 如果当前元素是数组,则递归处理
      sum += sumArray(arr[i]); 
    } else { 
      // 如果当前元素是数字,则加入总和
      sum += arr[i]; 
    } 
  } 
  return sum; 
} 
console.log(sumArray(nestedArray)); // 输出:10

此代码利用递归深入遍历嵌套数组 nestedArray。它可以计算所有数字的总和,而不受嵌套深度的限制。

递归函数允许自身反复调用,直到达到最底层的嵌套结构,并返回一个可用于计算最终总和的值。

通过检查每个元素是否为数组,该函数递归处理数组元素,直到达到最深的嵌套层级,即数字。

然后,函数将该数字返回到上一递归层。这样便于计算当前层级所有元素的总和。

通过这种方式,递归提供了一种灵活而优雅的方法,用于处理未知或不固定层级结构的问题。

结论

JavaScript 循环非常重要,但要精通使用它可能有一定的难度。遵循以上最佳实践,你将能够编写更高效、更合理的循环代码,进而提升整体的编程水准。
要选择合适的循环类型,并准确地使用循环控制语句来优化循环性能,避免常见的错误。同时,在适宜的场景下,你可以考虑递归的使用。只要掌握这些技巧,你就能大幅增强自己在 JavaScript 循环方面的专业技能。
因此,如果你投入时间来评估和优化循环代码,你的代码性能将得到显著提升。

译者介绍

刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号。

原文标题:JavaScript Loop: The Best Practices to Have the Optimal Performance,作者:robiulhr