Search code examples
javascriptiterationsparse-matrixfor-of-loop

Why does for-of not skip empty slots of a sparse Array? [JavaScript]


As the title says, why does for-of run the loop body with the loop variable bound to undefined for indices not in the Array, while other iteration constructs (forEach(), for-in, etc.) don't?

Clarification: As many misunderstood the question

It is not about:

  • iterating over TypedArrays (which cannot be sparse) or any other class
  • how to "correctly" iterate over a sparse Array (every other method seems to work in the expected way)
  • skipping undefined elements in an Array

Is the following informal description found on MDN incorrect?

The for...of statement [...] invokes a custom iteration hook with statements to be executed for the value of each distinct property of the object.

I.e. it is also invoked for non-existent properties.

const sparse = [0, 1, 2] // Was [0, , 2], but some are unfamiliar with this syntax 
                         // or think it creates the array [0, undefined, 2]
delete sparse[1]
for (let e of sparse) console.log('for-of', e)
// Contrast with:
sparse.forEach(e => console.log('forEach', e))
for (let i in sparse) console.log('for-in', sparse[i])
console.log('map', sparse.map(e => e)) // Note, prints incorrectly in the snippet
                                       // console, check browser console
// etc.

Is this the intended behaviour (Yes) & why was it designed in this way?


Solution

  • for..of calls the Array iterator method, which is described in the spec.

    (2) Let iterator be ObjectCreate(%ArrayIteratorPrototype%, «‍[[IteratedObject]], [[ArrayIteratorNextIndex]], [[ArrayIterationKind]]»).

    (4) Set iterator’s [[ArrayIteratorNextIndex]] internal slot to 0.

    Then, when the iterator is iterated over, in 22.1.5.2.1 %ArrayIteratorPrototype%.next::

    (6) Let index be the value of the [[ArrayIteratorNextIndex]] internal slot of O.

    (10) If index ≥ len, then

    (10) (a) Set the value of the [[IteratedObject]] internal slot of O to undefined.

    (10) (b) Return CreateIterResultObject(undefined, true).

    (11) Set the value of the [[ArrayIteratorNextIndex]] internal slot of O to index+1.

    (create iterator result object whose value is array[index])

    In other words - the iterator iterates starting at index 0, and increments the index by 1 every time .next() is called. It does not check to see if the array actually has an item at that index (which a sparse array will not) - it just checks that the index is less than the .length of the array.

    With for..in, on the other hand, all enumerable properties are iterated over, and an array's own enumerable properties do not include sparse array indicies.

    const sparse = [0, , 2];
    console.log(sparse.hasOwnProperty('0'));
    console.log(sparse.hasOwnProperty('1'));

    So yes, this is intended behavior.