ECMAScript 6: holes in Arrays

[2015-09-13] esnext, dev, javascript
(Ad, please don’t block)

This blog post describes how ECMAScript 6 handles holes in Arrays.

Holes in Arrays  

Holes are indices “inside” an Array that have no associated element. In other words: An Array arr is said to have a hole at index i if:

  • 0 ≤ i < arr.length
  • !(i in arr)

For example: The following Array has a hole at index 1.

> let arr = ['a',,'b']
'use strict'
> 0 in arr
true
> 1 in arr
false
> 2 in arr
true
> arr[1]
undefined

For more information, consult Sect. “Holes in Arrays” in “Speaking JavaScript”.

ECMAScript 6: holes are treated like undefined elements  

The general rule for Array methods that are new in ES6 is: each hole is treated as if it were the element undefined. Examples:

> Array.from(['a',,'b'])
[ 'a', undefined, 'b' ]
> [,'a'].findIndex(x => true)
0
> [...[,'a'].entries()]
[ [ 0, undefined ], [ 1, 'a' ] ]

The idea is to steer people away from holes and to simplify long-term. Unfortunately that means that things are even more inconsistent now.

Array operations and holes  

Array.from()  

Array.from() converts holes to undefined:

> Array.from(['a',,'b'])
[ 'a', undefined, 'b' ]

With a second argument, it works mostly like map(), but does not ignore holes:

> Array.from(new Array(3), (x,i) => i)
[ 0, 1, 2 ]

Spread operator (...)  

Inside Arrays, the spread operator (...) works much like Array.from() (but its operand must be iterable, whereas Array.from() can handle anything that’s Array-like).

> [...['a',,'b']]
[ 'a', undefined, 'b' ]

Array.prototype methods  

In ECMAScript 5, behavior already varied slightly. For example:

  • forEach(), filter(), every() and some() ignore holes.
  • map() skips but preserves holes.
  • join() and toString() treat holes as if they were undefined elements, but interprets both null and undefined as empty strings.

ECMAScript 6 adds new kinds of behaviors:

  • copyWithin() creates holes when copying holes (i.e., it deletes elements if necessary).
  • entries(), keys(), values() treat each hole as if it was the element undefined.
  • find() and findIndex() do the same.
  • fill() doesn’t care whether there are elements at indices or not.

The following table describes how Array.prototype methods handle holes.

Method Holes are
concat Preserved ['a',,'b'].concat(['c',,'d']) → ['a',,'b','c',,'d']
copyWithin Preserved [,'a','b',,].copyWithin(2,0) → [,'a',,'a']
entries Elements [...[,'a'].entries()] → [[0,undefined], [1,'a']]
every Ignored [,'a'].every(x => x==='a') → true
fill Filled new Array(3).fill('a') → ['a','a','a']
filter Removed ['a',,'b'].filter(x => true) → ['a','b']
find Elements [,'a'].find(x => true) → undefined
findIndex Elements [,'a'].findIndex(x => true) → 0
forEach Ignored [,'a'].forEach((x,i) => log(i)); → 1
indexOf Ignored [,'a'].indexOf(undefined) → -1
join Elements [,'a',undefined,null].join('#') → '#a##'
keys Elements [...[,'a'].keys()] → [0,1]
lastIndexOf Ignored [,'a'].lastIndexOf(undefined) → -1
map Preserved [,'a'].map(x => 1) → [,1]
pop Elements ['a',,].pop() → undefined
push Preserved new Array(1).push('a') → 2
reduce Ignored ['#',,undefined].reduce((x,y)=>x+y) → '#undefined'
reduceRight Ignored ['#',,undefined].reduceRight((x,y)=>x+y) → 'undefined#'
reverse Preserved ['a',,'b'].reverse() → ['b',,'a']
shift Elements [,'a'].shift() → undefined
slice Preserved [,'a'].slice(0,1) → [,]
some Ignored [,'a'].some(x => x !== 'a') → false
sort Preserved [,undefined,'a'].sort() → ['a',undefined,,]
splice Preserved ['a',,].splice(1,1) → [,]
toString Elements [,'a',undefined,null].toString() → ',a,,'
unshift Preserved [,'a'].unshift('b') → 3
values Elements [...[,'a'].values()] → [undefined,'a']

Notes:

  • ES6 methods have checkmarks (✓).
  • JavaScript ignores a trailing comma in an Array literal: ['a',,].length → 2
  • Helper function used in the table: const log = console.log.bind(console);

Recommendations  

With regard to holes in Arrays, the only rule is now that there are no rules. Therefore, you should avoid holes if you can (they affect performance negatively, too). If you can’t then the table in the previous section may help.

Further reading  

  • ECMAScript 5: Chapter “Arrays” in “Speaking JavaScript”
  • ECMAScript 6: Chapter “New Array features” in “Exploring ES6”