DEV Community

Discussion on: Why is using javascript “for loop” for array iteration a bad idea?

Collapse
 
peerreynders profile image
peerreynders • Edited

The idiomatic way to detect holes in arrays is with the in operator;

const a = [1, 2, 3, ,5];

for(let i = 0; i < a.length; i += 1)
  console.log(i in a ? a[i] : 'hole'); // 1, 2, 3, 'hole', 5
Enter fullscreen mode Exit fullscreen mode

If you need to be paranoid about third party libraries modifying prototype objects:

// https://eslint.org/docs/rules/no-prototype-builtins
const hasOwnProperty = Object.prototype.hasOwnProperty;
const a = [1, 2, 3, , 5];

for (let i = 0; i < a.length; i += 1)
  console.log(hasOwnProperty.call(a, i) ? a[i] : 'hole'); // 1, 2, 3, 'hole', 5
Enter fullscreen mode Exit fullscreen mode

For a more detailed discussion of holes see ECMAScript 6: holes in Arrays.

Also you can try .map() method instead of for..loop.

While map() doesn't call the iteratee on holes it does preserve the holes in the array it returns. The point being - the use case for map() (producing a new array of identical length containing transformed values) is much narrower than that of a for…loop. The behaviour of map() is consistent with treating holes as None while values are treated as Some(value). This is problematic when one needs to replace the holes with some default value - in that case it is necessary to use Array.from():

const a = [1, 2, 3, , 5];
const transform = (x) => (typeof x !== 'undefined' ? x * 2 : 0);

console.log(a.map(transform)); // [2, 4, 6, empty, 10]
console.log(Array.from(a, transform)); // [2, 4, 6, 0, 10]
Enter fullscreen mode Exit fullscreen mode

Ultimately the entire for…loop considered harmful attitude is misguided. Iteration is often required without arrays being involved. So it is valuable to be able to write a readable for…loop.

Higher order functions (HOFs) like reduce(), map() and filter() have their uses but forEach() is the oddball as it needs a function with side effects to be useful.

Iteration on iterables doesn't have direct access to the Array HOFs, so either they need to be converted to intermediate arrays (e.g. with Array.from()) or be processed with for…of.

forEach() exists on Maps, Sets, DOMtokenList, NodeList, TypedArray, etc.

But I fail to see the advantage of

let node = document.createElement('div');
let kid1 = document.createElement('p');
let kid2 = document.createTextNode('hey');
let kid3 = document.createElement('span');

node.appendChild(kid1);
node.appendChild(kid2);
node.appendChild(kid3);

let list = node.childNodes;

list.forEach(function (value, index, _list) {
  console.log(`${value}, ${index}, ${this}`);
}, 'myThisArg');
Enter fullscreen mode Exit fullscreen mode

over

let list = node.childNodes;
const context = 'myThisArg';

for (let i = 0; i < list.length; i += 1)
  console.log(`${list[i]}, ${i}, ${context}`);
Enter fullscreen mode Exit fullscreen mode

or perhaps

let list = node.childNodes;
const context = 'myThisArg';

for (const item of list) console.log(`${item}, ${context}`);
Enter fullscreen mode Exit fullscreen mode

Perhaps the real solution is to clean up the loop bodies of for…loop so that they are easier to read.

In functional languages recursion is used as the fundamental means of any iteration:

function fibRec(n1, n, i) {
  return i > 1 ? fibRec(n, n1 + n, i - 1) : n;
}

function fib(lastIndex) {
  return lastIndex > 0 ? fibRec(0, 1, lastIndex) : 0;
}

const a = Array.from({ length: 21 }, (_v, i) => [i, fib(i)]);
console.log(a);

/* [
  [0, 0], [1, 1], [2, 1], [3, 2], [4, 3], 
  [5, 5], [6, 8], [7, 13], [8, 21], [9, 34], 
  [10, 55], [11, 89], [12, 144], [13, 233], [14, 377], 
  [15, 610], [16, 987], [17, 1597], [18, 2584], [19, 4181], 
  [20, 6765]
  ] */
Enter fullscreen mode Exit fullscreen mode

This works in JavaScript but JavaScript isn't a functional language - and none of today's JavaScript engines support tail call elimination, so recursion is usually avoided to avoid blowing the call stack (and sometimes to avoid the overhead of the function calls).
The fundamental means of iteration in JavaScript is the for…loop which means that mutation of the looping state is essential but the rest of the values can largely be immutable:

function fib(lastIndex) {
  let n1 = 0;
  if (lastIndex < 1) return n1;

  let n = 1;
  for (let i = lastIndex; i > 1; i -= 1) {
    const next = n1 + n; // [1]
    [n1, n] = [n, next]; // [2]
  }
  return n;
}

const a = Array.from({ length: 21 }, (_v, i) => [i, fib(i)]);
console.log(a);
Enter fullscreen mode Exit fullscreen mode

i.e. for a cleaner loop body:

  • [1] the majority of the loop body sticks to immutable values ...
  • [2] ... to only later mutate the loop state values in preparation for the next cycle

The idea is to organize the top of the loop body so that it could be easily refactored to:

function next(n1, n) {
  return [n, n1 + n];
}

function fib(lastIndex) {
  let n1 = 0;
  if (lastIndex < 1) return n1;

  let n = 1;
  for (let i = lastIndex; i > 1; i -= 1) [n1, n] = next(n1, n);

  return n;
}

const a = Array.from({ length: 21 }, (_v, i) => [i, fib(i)]);
console.log(a);
Enter fullscreen mode Exit fullscreen mode

but to then leave the loop body "as is" because it already is sufficiently cleaned up.

Collapse
 
peerreynders profile image
peerreynders • Edited
const length = 1000000;

let start = performance.now();
const a = new Array(length);
let finish = performance.now();
console.log(`Sparse array was created in ${finish - start}ms`); // < 5ms

start = performance.now();
const b = Array.from({ length });
finish = performance.now();
console.log(`Dense array was created in ${finish - start}ms`); // ~60ms

start = performance.now();
const c = [];
c[length - 1] = 1;
finish = performance.now();
console.log(`Element added in ${finish - start}ms`); // < 1ms
console.log(c[length - 1]); // 1
console.log(c.length); // 1000000
Enter fullscreen mode Exit fullscreen mode

Basically think of arrays as key-value stores that use positive integers as keys.


The problem is that JavaScript engines optimize arrays for performance

v8.dev: Elements kinds in V8:

  • Small Integers: PACKED_SMI_ELEMENTS -> HOLEY_SMI_ELEMENTS
  • Doubles: PACKED_DOUBLE_ELEMENTS -> HOLEY_DOUBLE_ELEMENTS
  • Other: PACKED_ELEMENTS -> HOLEY_ELEMENTS .
  • PACKED (dense) arrays can transition to HOLEY (sparse) arrays - but not the other way around
  • PACKED processing is more optimized, though:

the performance difference between accessing holey or packed arrays is usually too small to matter or even be measurable.

That said (as already suggested elsewhere), if holes don't have any particular meaning within the processing context, it's better to sanitize HOLEY arrays into PACKED arrays rather than relying on the "skipping behaviour" of select methods.