This is V8 behaviour, per V8's elements-kinds blog and checked on Node 24.19 (V8 13.6). The language only specifies that arrays behave like objects with index keys.
V8 keeps array-indexed properties in an elements store, separate from named properties (fast properties). The store's representation is tracked as the array's elements kind:
- Value kind:
SMI(small integers),DOUBLE(numbers, stored as raw doubles), or plainELEMENTS(anything else, including objects andundefined). - Packing:
PACKED(no holes) orHOLEY(at least one missing index).
That gives PACKED_SMI_ELEMENTS, PACKED_DOUBLE_ELEMENTS, PACKED_ELEMENTS and the three HOLEY_ forms. There are also other kinds: a very sparse array moves to DICTIONARY_ELEMENTS, a hash table.
Transitions are one-way, from specific to general: Smi to double to generic, and packed to holey. Pushing 4.5 into a Smi array makes it double; pushing a string makes it generic. Emptying the array does not reset the kind. Per the blog, -0, NaN and Infinity count as doubles, so one of them downgrades a Smi array.
Why it matters. Code and inline caches specialise on the kind, so a function that sees several kinds is polymorphic, just like with object shapes. Reading a hole forces a prototype-chain lookup, because arr[i] on a missing index may find a value on Array.prototype. Packed arrays avoid that check.
Habits: build arrays with literals or push, not new Array(n) followed by out-of-order writes; keep one value type per hot array; do not read past the end.