This blog post describes how ECMAScript 6 handles 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:
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”.
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.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 ]
...
) 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:
['a',,].length → 2
const log = console.log.bind(console);
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.