r-d-smith-taQV2u1JM_4-unsplash.jpg

The Dark Side of 'Foreach()' : Why You Should Think Twice Before Using It

The other day, I was writing some code, and it just wasn't working for me. I was using forEach() to iterate over an array of objects and then make an API call for one. Some of you probably recognize my mistake - you can't use async functions inside a forEach()! A friend of mine pointed out the problem, and I reverted back to a good old for loop, but it still bugged me that it wasted my time.

I've known for a long time that forEach() is less performant than for, and that there are other issues (see below), but I prefer to make my code as "readable" as possible, and in this case the forEach() was a much clearer way to explain what I was doing. Since this is something that I'm sure other people have run into, I decided to write a quick post about it... and then I started searching. Turns out, there are lots of other issues. 

So, I collected a list of the biggest limitations of the method, and broke it down in this article with examples and code samples.

  1. Performance Issues: forEach() is 95% slower than a traditional for loop.
  2. Inability to Escape the Loop: You can't use break or continue within forEach().
  3. Incompatibility with Async Functions: forEach() doesn't work well with async/await.
  4. Better Options for Arrays: map(), reduce(), and filter() are often more appropriate.
  5. Lack of Return Value: forEach() always returns undefined.
  6. Handling Sparse Arrays: forEach() skips over empty slots in sparse arrays.
  7. Practical Examples and Case Studies: Real-world scenarios where forEach() falls short.

Hopefully, this will give you a more complete understanding of the limitations of forEach() and be better equipped to choose the right approach for your specific use cases.

1. Performance Issues

Comparison with for Loop

One of the biggest limitations of the forEach() function is its performance compared to traditional for loops. This becomes particularly evident in large-scale applications where even minor inefficiencies can lead to noticeable slowdowns.

Benchmarking Performance

To understand the performance difference, let's compare forEach() with a traditional for loop through a series of benchmarks. You can cut/paste this code into your console to see the results directly.

Code Sample: forEach() Performance Test

let largeArray = Array.from({ length: 1e6 }, (_, i) => i);

console.time('forEach');
largeArray.forEach((num) => {
  // Perform a simple operation
  let result = num * 2;
});
console.timeEnd('forEach');

// Output: 
// forEach: 9.346923828125 ms

Code Sample: for Loop Performance Test

let largeArray = Array.from({ length: 1e6 }, (_, i) => i);

console.time('for');
for (let i = 0; i < largeArray.length; i++) {
  // Perform a simple operation
  let result = largeArray[i] * 2;
}
console.timeEnd('for');

// Output: 
// for: 2.3388671875 ms

When you run these benchmarks, you will notice a substantial difference in execution time. In this case, we can see that for is four times faster. Typically, the for loop outperforms forEach() by a significant margin. There are several reasons for this:

  1. Function Overhead: forEach() invokes a callback function for each element, which introduces additional function call overhead.
  2. Iterator Protocol: The underlying iterator protocol used by forEach() can add to the execution time.
  3. Optimization: Traditional for loops are more straightforward for JavaScript engines to optimize compared to higher-order functions like forEach().

Real-World Scenarios

This performance difference can have a substantial impact in real-world applications, especially those dealing with large datasets or performance-critical operations. For instance, in data processing applications where arrays with millions of elements are common, using forEach() could lead to noticeable slowdowns.

Example: Data Processing

let processData = (data) => {
  console.time('forEach');
  data.forEach((item) => {
    // Simulate a complex operation
    item.processed = item.value * 2;
  });
  console.timeEnd('forEach');

  console.time('for');
  for (let i = 0; i < data.length; i++) {
    // Simulate a complex operation
    data[i].processed = data[i].value * 2;
  }
  console.timeEnd('for');
};

let largeDataset = Array.from({ length: 1e6 }, (_, i) => ({ value: i }));
processData(largeDataset);

// Output: 
// forEach: 167.56103515625 ms
// for: 6.554931640625 ms

In this example, processing the data with a traditional for loop is considerably faster than using forEach() - twenty-five times faster! In the above example, being 4 times faster is good, but 25 times faster means no contest.

Implications for Large-Scale Applications

For large-scale applications or anytime performance is a critical factor, you should carefully consider the choice of iteration methods. While forEach() offers a cleaner and more readable syntax (which I do like!), the performance cost might be too high, especially with extensive data processing requirements. This is reason enough to probably use a for loop as your default, even if the code isn't quite as clean.

Functional Limitations

While convenient for iterating over arrays, forEach()  has a few key limitations that make it a deal breaker in some cases. We will look at two major functional constraints here: the inability to escape the loop and the incompatibility with asynchronous functions.

2. Inability to Escape the Loop

One of the key limitations of the forEach() function is that it does not support the use of control flow statements like break or continue. This means that once forEach() starts iterating over an array, it will execute the provided callback function for each element without interruption. This can be a problem when you need to exit the loop early or skip certain loops based on specific conditions.

In a traditional for loop, you can use break to exit the loop early or continue to skip to the next iteration. However, these control flow statements are not available in forEach().

Example: Exiting a Loop Early with for

const numbers = [1, 2, 3, 4, 5];

for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] === 3) {
    break; // Exit the loop early
  }
  console.log(numbers[i]);
}
// Output: 1, 2

Example: Attempting to Exit Early with forEach()

const numbers = [1, 2, 3, 4, 5];

numbers.forEach((num) => {
  if (num === 3) {
    return; // This will only exit the current callback, not the entire loop
  }
  console.log(num);
});
// Output: 1, 2, 4, 5 (3 is skipped, but the loop continues)

As seen in the above examples, the inability to use break or continue in forEach() will result in running all iterations and potentially performing redundant calls. This is another potential performance hit on top of the base issues we outlined above!.

Alternatives for Loop Control

Alternative loop constructs should be considered when you need to control the flow of iterations. The for or for...of loops give you all the control you need and are more suitable for situations requiring early exit or iteration skipping.

Example: Using for...of with break

const numbers = [1, 2, 3, 4, 5];

for (const num of numbers) {
  if (num === 3) {
    break; // Exit the loop early
  }
  console.log(num);
}
// Output: 1, 2

By using for...of, you get total control over the loop flow, which is not possible with forEach().

3. Incompatibility with Async Functions

Another significant limitation of forEach() is its incompatibility with asynchronous functions. This is what prompted me to write an article in the first place! 

The forEach() method does not await the execution of promises within its callback, which pretty much makes async calls unusable. This can result in the loop completing before all asynchronous operations are finished. You might get lucky in some cases, but you will eventually see a problem. Even worse, the problem would be inconsistent, so a nightmare to debug. 

Example: Async Operations with forEach()

let fetchData = async (id) => {
  // Simulate an asynchronous API call
  return new Promise((resolve) => setTimeout(() => resolve(`Data for ${id}`), 1000));
};

let ids = [1, 2, 3, 4, 5];

ids.forEach(async (id) => {
  let data = await fetchData(id);
  console.log(data);
});
// Output: Likely an empty array or undefined

In the example above, the forEach() method does not wait for the fetchData promise to resolve, leading to a situation where the loop completes before any data is logged.

Workarounds and Alternatives

To handle asynchronous operations within a loop, alternative constructs like for...of and map() combined with Promise.all() can be used.

Example: Using for...of with async/await

async function fetchData(id) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(`Data for ${id}`);
    }, 1000);
  });
}

let ids = [1, 2, 3, 4, 5];

async function processIds() {
  for (id of ids) {
    let data = await fetchData(id);
    console.log(data);
  }
}

processIds();
// Output: Data for 1, Data for 2, ..., Data for 5 (each logged after 1 second delay)

Example: Using map() with Promise.all()

async function fetchData(id) {
  return new Promise(function(resolve) {
    setTimeout(function() {
      resolve(`Data for ${id}`);
    }, 1000);
  });
}

let ids = [1, 2, 3, 4, 5];

async function processIds() {
  let promises = ids.map(fetchData);
  let results = await Promise.all(promises);
  results.forEach(logData);
}

function logData(data) {
  console.log(data);
}

processIds();
// Output: Data for 1, Data for 2, ..., Data for 5 (all logged at once after 1 second delay)

By using for...of or map() with Promise.all(), you can handle asynchronous operations more effectively, ensuring that all promises are resolved before proceeding.

4. Better Options for Arrays

While the forEach() function is a straightforward way to iterate over arrays, it is not always the best tool for array manipulation. In many cases, other array methods, such as map(), reduce(), and filter() provide more efficient and readable solutions for transforming and working with arrays. 

When to Use map(), reduce(), and filter()

map()

The map() method is used for creating a new array by applying a function to each element of the original array. It is handy for transforming data without mutating the original array.

Example: Using forEach() to Transform an Array

let numbers = [1, 2, 3, 4, 5];
let doubledNumbers = [];

numbers.forEach((num) => {
  doubledNumbers.push(num * 2);
});

console.log(doubledNumbers); // [2, 4, 6, 8, 10]

Example: Using map() to Transform an Array

let numbers = [1, 2, 3, 4, 5];
let doubledNumbers = numbers.map((num) => num * 2);

console.log(doubledNumbers); // [2, 4, 6, 8, 10]

As you can see, map() not only makes the code more concise but also avoids the need to handle the creation of a new array manually. This is faster and much easier to use!

reduce()

The reduce() method is used to aggregate array values into a single output, such as summing numbers, flattening arrays, or creating more complex data structures. It applies a function against an accumulator and each element in the array to reduce it to a single value.

Example: Using forEach() to Sum Array Values

let numbers = [1, 2, 3, 4, 5];
let sum = 0;

numbers.forEach((num) => {
  sum += num;
});

console.log(sum); // 15

Example: Using reduce() to Sum Array Values

let numbers = [1, 2, 3, 4, 5];
let sum = numbers.reduce((acc, num) => acc + num, 0);

console.log(sum); // 15

reduce() provides a clear and functional approach to aggregating data, making the code more readable and expressive.

filter()

The filter() method is used to create a new array with elements that pass a specified test. It is really useful for extracting subsets of data based on certain conditions.

Example: Using forEach() to Filter an Array

let numbers = [1, 2, 3, 4, 5];
let evenNumbers = [];

numbers.forEach((num) => {
  if (num % 2 === 0) {
    evenNumbers.push(num);
  }
});

console.log(evenNumbers); // [2, 4]

Example: Using filter() to Filter an Array

let numbers = [1, 2, 3, 4, 5];
let evenNumbers = numbers.filter((num) => num % 2 === 0);

console.log(evenNumbers); // [2, 4]

Just like the examples above, using filter() simplifies the code and makes it more intuitive to understand the logic.

Performance and Readability Benefits

Using these higher-order functions makes the code more concise and can lead to performance improvements due to better optimization by JavaScript engines. These methods promote a functional programming style for extra cool kid points, which can enhance code readability and maintainability.

Readability and Maintenance

When writing code, readability and maintainability are crucial factors to consider. Cleaner code is easier to understand, debug, and extend. Now we can look at how you can chain these methods to powerup your code and go even further beyond what forEach() can do on its own.

Code Readability

Example: Using forEach() for Multiple Transformations

let numbers = [1, 2, 3, 4, 5];
let transformedNumbers = [];

numbers.forEach((num) => {
  if (num % 2 === 0) {
    transformedNumbers.push(num * 2);
  }
});

console.log(transformedNumbers); // [4, 8]

Example: Using filter() and map() for Multiple Transformations

let numbers = [1, 2, 3, 4, 5];
let transformedNumbers = numbers
  .filter((num) => num % 2 === 0)
  .map((num) => num * 2);

console.log(transformedNumbers); // [4, 8]

By chaining filter() and map(), the code is easier to understand. It just looks better, and we know it will be faster.

Maintainability

Example: Extending forEach() Logic

let numbers = [1, 2, 3, 4, 5];
let result = [];

numbers.forEach((num) => {
  if (num % 2 === 0) {
    result.push(num * 2);
  }
  if (num > 2) {
    result.push(num + 1);
  }
});

console.log(result); // [4, 8, 4, 5, 6]

Ooof - those nested ifs inside the loop is pretty smelly. It makes it much harder to understand what is going on, and heaven help you when you need to come back and change it in six months. This is basically writing code that is setting a trap for yourself.

Example: Extending Logic with Separate Functions

let numbers = [1, 2, 3, 4, 5];

function doubleEvenNumbers(num) {
  return num % 2 === 0 ? num * 2 : null;
}

function incrementNumbersGreaterThanTwo(num) {
  return num > 2 ? num + 1 : null;
}

function isNotNull(value) {
  return value !== null;
}

let doubledEvens = numbers
	.map(doubleEvenNumbers)
	.filter(isNotNull);
let incrementedNumbers = numbers
	.map(incrementNumbersGreaterThanTwo)
	.filter(isNotNull);

let result = doubledEvens.concat(incrementedNumbers);

console.log(result); // [4, 8, 4, 5, 6]

By separating the logic into distinct functions and using map() and filter(), the code becomes modular and easier to maintain. Each function is responsible for a specific transformation, making it easier to update and debug.

Best Practices for Maintaining Clean and Readable Code

  • Use Higher-Order Functions: Whenever possible, use map(), reduce(), and filter() for their intended purposes. They make the code more declarative and easier to understand.
  • Keep Functions Small and Focused: Break down complex transformations into smaller, reusable functions.
  • Avoid Mutating State: Prefer returning new arrays or values instead of mutating existing ones. This promotes immutability and reduces side effects.
  • Document Intent: Use comments and meaningful function names to document the intent behind transformations.

By following these best practices, you can ensure that your code remains clean, readable, and maintainable, making it easier to work with in the long run.

Other Limitations

Beyond performance issues and functional limitations, the forEach() function has several other limitations that can impact its usability and effectiveness in various scenarios. Let's look at two additional limitations: the lack of a return value and the handling of sparse arrays.

5. Lack of Return Value

One of the fundamental characteristics of the forEach() function is that it always returns undefined. This limitation means that forEach() cannot be used in contexts where a return value is expected or needed.

In many situations, returning a value, either the transformed array or a specific aggregated result is helpful. Since forEach() does not provide a return value, it means you have to use external variables to store results, which can lead to less functional and more imperative code.

Example: Using forEach() Without Return Value

let numbers = [1, 2, 3, 4, 5];
let doubledNumbers = [];

numbers.forEach((num) => {
  doubledNumbers.push(num * 2);
});

console.log(doubledNumbers); // [2, 4, 6, 8, 10]

In this example, doubledNumbers must be declared and managed outside the forEach() callback, which adds extra complexity and potential for errors.

Comparison with Other Methods

Other array methods like map(), reduce(), and filter() return values that can be directly used or chained with other operations, promoting a more functional programming style.

Example: Using map() with Return Value

let numbers = [1, 2, 3, 4, 5];
let doubledNumbers = numbers.map((num) => num * 2);

console.log(doubledNumbers); // [2, 4, 6, 8, 10]

By using map(), the transformation and the assignment to a new array are handled in a single, concise statement.

Example: Refactoring forEach() with reduce() for Aggregation

let numbers = [1, 2, 3, 4, 5];
let sum = 0;

numbers.forEach((num) => {
  sum += num;
});

console.log(sum); // 15

Example: Using reduce() for Aggregation

let numbers = [1, 2, 3, 4, 5];
let sum = numbers.reduce((acc, num) => acc + num, 0);

console.log(sum); // 15

Using reduce() not only simplifies the code but also makes the aggregation operation more explicit and functional.

6. Handling Sparse Arrays

Sparse arrays are arrays with "holes" where some elements are missing or undefined. The behavior of forEach() with sparse arrays can be counterintuitive and lead to unexpected results.

forEach() skips over sparse elements in an array, meaning that the callback function is not called for missing or undefined elements. If you don't understand this, or are counting on each item being processed even if they are empty, then you're gonna have a bad time.

Example: Handling Sparse Arrays with forEach()

let sparseArray = [1, , 3, , 5];

sparseArray.forEach((num, index) => {
  console.log(`Index ${index}: ${num}`);
});
// Output:
// Index 0: 1
// Index 2: 3
// Index 4: 5

In this example, the forEach() method skips the holes in the sparse array, which might not be what you want.

Alternative Methods

Other iteration methods like for, for...of, and map() do not skip sparse elements, making them more reliable.

Example: Handling Sparse Arrays with for...of

let sparseArray = [1, , 3, , 5];

for (const [index, num] of sparseArray.entries()) {
  console.log(`Index ${index}: ${num}`);
}
// Output:
// Index 0: 1
// Index 1: undefined
// Index 2: 3
// Index 3: undefined
// Index 4: 5

Using for...of with entries() provides a complete iteration over the array, including the sparse elements.

Example: Handling Sparse Arrays with map()

let sparseArray = [1, , 3, , 5];

let filledArray = sparseArray.map((num, index) => num === undefined ? null : num);

console.log(filledArray); // [1, null, 3, null, 5]

With map(), you can handle sparse elements more explicitly, filling them with a default value if necessary.

Example: Ensuring Correctness with reduce()

let sparseArray = [1, , 3, , 5];

let sum = sparseArray.reduce((acc, num) => acc + (num || 0), 0);

console.log(sum); // 9

By using reduce(), you can aggregate values from a sparse array while ensuring that missing elements do not affect the result.

7. Practical Examples and Case Studies

To really understand the limitations of the forEach() function, let's look at some real-world scenarios where these limitations cause problems. In this section, we will look at practical examples and case studies where using forEach() has led to performance or functionality issues. We will also explore how refactoring the code using alternative methods improved the code's performance, readability, and maintainability.

Case Study 1: Performance Bottleneck in Data Processing

Scenario: A developer needs to process a large dataset containing millions of records. The initial implementation uses forEach() to iterate over the array and perform some computationally intensive operations.

Initial Implementation with forEach()

let largeArray = Array.from({ length: 1e6 }, (_, i) => i);
let results = [];

console.time('forEach');
largeArray.forEach((num) => {
  // Perform a complex operation
  let result = num * 2;
  results.push(result);
});
console.timeEnd('forEach');

// Output: 
// forEach: 39.712158 ms

Issues:

  • Performance: The forEach() implementation is much slower due to the overhead of the callback function.
  • Memory Usage: The results array grows in size as the loop progresses, consuming more memory.

Refactored Implementation with for Loop

let largeArray = Array.from({ length: 1e6 }, (_, i) => i);
let results = [];

console.time('for');
for (let i = 0; i < largeArray.length; i++) {
  // Perform a complex operation
  let result = largeArray[i] * 2;
  results.push(result);
}
console.timeEnd('for');

// Output: 
// for: 18.368896 ms

Benefits:

  • Improved Performance: The for loop reduces the execution time.
  • Better Control: The developer can easily manage the loop flow and optimize memory usage.

Case Study 2: Asynchronous Operations in Loop

Scenario: A web application needs to fetch data from an API for each item in an array. The initial implementation uses forEach() with async/await, expecting the loop to handle asynchronous operations correctly.

Initial Implementation with forEach()

let fetchData = async (id) => {
  // Simulate an asynchronous API call
  return new Promise((resolve) => setTimeout(() => resolve(`Data for ${id}`), 1000));
};

let ids = [1, 2, 3, 4, 5];

ids.forEach(async (id) => {
  let data = await fetchData(id);
  console.log(data);
});

// Output: Likely an empty array or undefined

Issues:

  • Asynchronous Handling: The forEach() loop does not wait for the asynchronous operations to complete, leading to incorrect or incomplete results.

Refactored Implementation with for...of Loop

async function fetchData(id) {
  return new Promise(resolve => setTimeout(() => resolve(`Data for ${id}`), 1000));
}

let ids = [1, 2, 3, 4, 5];

async function processIds() {
  for (let id of ids) {
    let data = await fetchData(id);
    console.log(data);
  }
}

processIds();
// Output: Data for 1, Data for 2, ..., Data for 5 (each logged after 1 second delay)

Benefits:

  • Correct Asynchronous Handling: The for...of loop ensures that each asynchronous operation completes before moving to the next iteration.
  • Readable Code: The code is easier to understand and maintain.

Case Study 3: Complex Data Transformations

Scenario: A developer needs to perform multiple transformations on an array of objects, such as filtering, mapping, and reducing data. The initial implementation uses forEach() for each transformation step.

Initial Implementation with forEach()

let items = [
  { value: 1, active: true },
  { value: 2, active: false },
  { value: 3, active: true },
  { value: 4, active: true },
  { value: 5, active: false }
];

let activeItems = [];
let totalValue = 0;

items.forEach((item) => {
  if (item.active) {
    activeItems.push(item);
  }
});

activeItems.forEach((item) => {
  totalValue += item.value;
});

console.log(activeItems); // [{ value: 1, active: true }, { value: 3, active: true }, { value: 4, active: true }]
console.log(totalValue); // 8

Issues:

  • Redundancy: Multiple forEach() loops make the code redundant and harder to maintain.
  • Performance: Iterating over the array multiple times is less efficient.

Refactored Implementation with filter(), map(), and reduce()

let items = [
  { value: 1, active: true },
  { value: 2, active: false },
  { value: 3, active: true },
  { value: 4, active: true },
  { value: 5, active: false }
];

let activeItems = items.filter(item => item.active);
let totalValue = activeItems.reduce((sum, item) => sum + item.value, 0);

console.log(activeItems); // [{ value: 1, active: true }, { value: 3, active: true }, { value: 4, active: true }]
console.log(totalValue); // 8

Benefits:

  • Concise Code: The use of filter() and reduce() makes the code more concise and easier to understand.
  • Improved Performance: The array is only iterated over twice, improving performance.
  • Maintainability: The functional approach is more maintainable and scalable.

So - when should you use it?

At this point, you may be wondering if you should use forEach() at all. Honestly, most of the time you probably shouldn't. It really only makes sense when you need to perform operations on each element of an array without modifying the array itself or returning a new array. 

Here are some guidelines on when you might want to use forEach():

  1. Side Effects Only:
    • Use forEach() when you need to execute side effects (such as logging, updating external variables, or performing DOM manipulations) on each element of an array.
  2. No Need for Return Values:
    • Use forEach() when you do not need a return value or a transformed array from the iteration.
  3. Mutating External Variables:
    • Use forEach() when you need to mutate external variables within the callback function.
  4. Simple Iteration Tasks:
    • Use forEach() for simple iteration tasks where readability and simplicity are more important than performance.
  5. No Asynchronous Operations:
    • Use forEach() for synchronous operations only, as it does not handle asynchronous callbacks effectively.
  6. No Requirement for Index:
    • Use forEach() when you do not need the index of the current element in the iteration.

This list should raise some red flags for most developers, especially those who want to write more functional code that is less likely to lead to bugs. I am reluctant to say you should never use it... but I do think it is safe to say that it is best to avoid it. I'd say using a more direct array method (or a chain of them) should be the first thing to do, and then you can fall back to using a for loop if all else fails. 

You know then that you will be avoiding potential issues, edge cases, and future bugs... especially when another developer comes along and extends your code. 

Joseph Petty Verified userVerified user staff View joseph_appsmith's profile
Thu, 06/13/2024 - 18:58

Excellent post! I knew about a few of the limitations but the amount of performance difference was surprising. 

Kingstone Job public View kingstonejob's profile
Sat, 06/15/2024 - 02:25

Are these limitations of Foreach Loop applicable in PHP, Java and C#?

Ron Northcutt Verified userVerified user staff View ron's profile
Mon, 06/17/2024 - 09:19

In reply to by kingstonejob

This only applies to Javascript. I'm not sure how this applies to other languages.

phạmquanghuy public View phạmquanghuy's profile
Wed, 06/19/2024 - 03:43

When it comes to performance, for of is even much slower than forEach

Ron Northcutt Verified userVerified user staff View ron's profile
Wed, 06/19/2024 - 17:57

In reply to by phạmquanghuy

I'm not so sure. The best I can find is that for is pretty much always the fastest, and for of only seems to be a slight issue with massive datasets (far beyond what a frontend app would do). And for of is still faster than forEach

In any event, forEach still has way too many other limitations, and for of is much more readable than for when you don't know the size of the array you are iterating over. 

See: 

ryansmith public View ryansmith's profile
Thu, 06/20/2024 - 13:34

Do map and reduce have the same performance overhead relative to a basic for loop?

The difference in behavior for undefined elements in the array is a deal killer, however. 98% of the time, developers will fail to remember this issue, even if they read about it before.

marcinlis public View marcinlis's profile
Sun, 07/14/2024 - 10:18

I tried this on myself... Below is my results.

Sometimes for of is slower than forEach, Map is definitely slowest. I tested it in a few order for more accurate, but map is always a slowest and results is similar like below.
Tested on Mac Studio M1 Max.

First results:
map: 31.096923828125 ms
forEach: 14.196044921875 ms
forof: 16.885986328125 ms
 for: 6.1611328125 ms
forsepconst: 6.281982421875 ms

Second results:
map: 27.2080078125 ms
forEach: 17.361083984375 ms
forof: 14.570068359375 ms
for: 6.177001953125 ms
forsepconst: 6.3369140625 ms

Third results, with uncommented querySelectorAll:
map: 2184.64697265625 ms
forEach: 2154.363037109375 ms
forof: 2177.132080078125 ms
for: 2176.41015625 ms
forsepconst: 2166.047119140625 ms

{
    console.time('map');
    // eslint-disable-next-line no-unused-vars
    const results = largeArray.map((num) => {
      //document.querySelectorAll('.b-example');
      return (((num * 2 - 5 / 0.5 + 1) * 2) / 3) * num;
    });
    console.timeEnd('map');
  }
  {
    const results = [];

    console.time('forEach');
    largeArray.forEach((num) => {
      //document.querySelectorAll('.b-example');
      results.push((((num * 2 - 5 / 0.5 + 1) * 2) / 3) * num);
    });
    console.timeEnd('forEach');
  }
  {
    const results = [];

    console.time('forof');
    for (const num of largeArray) {
      //document.querySelectorAll('.b-example');
      results.push((((num * 2 - 5 / 0.5 + 1) * 2) / 3) * num);
    }
    console.timeEnd('forof');
  }
  {
    const results = [];

    console.time('for');
    for (let i = 0; i < largeArray.length; i++) {
      //document.querySelectorAll('.b-example');
      results.push((((largeArray[i] * 2 - 5 / 0.5 + 1) * 2) / 3) * largeArray[i]);
    }
    console.timeEnd('for');
  }
  {
    const results = [];

    console.time('forsepconst');
    for (let i = 0; i < largeArray.length; i++) {
      const num = largeArray[i];
      //document.querySelectorAll('.b-example');
      results.push((((num * 2 - 5 / 0.5 + 1) * 2) / 3) * num);
    }
    console.timeEnd('forsepconst');
  }