What survives

Does obfuscation break sparse arrays and sort order?

Two array behaviours are easy to depend on without ever writing them down: that a hole is not the same as an undefined value, and that a sort with equal keys leaves those items in their original relative order. Both are the sort of thing a rewrite could plausibly disturb. Measured across five configurations, neither moves.

Two behaviours that are easy to depend on by accident

A sparse array is one with gaps. Writing [1, , 3] or assigning to an index past the end produces an array whose length counts positions that were never assigned. A hole is not the same as a position holding undefined, and the difference is observable: the in operator reports false for a hole, forEach skips it entirely, and join renders it as empty text.

Sort stability is the second. Since ES2019 Array.prototype.sort is required to be stable, which means two items your comparator calls equal come out in the order they went in. A great deal of user-facing code depends on this without saying so, most obviously any table that supports sorting by one column after another and expects the previous ordering to act as a tiebreak.

Neither behaviour is usually written into a test, which is exactly why they are worth measuring rather than assuming. A regression in either would surface as a subtly wrong list rather than as an exception.

What was measured

The sample covered array shape and array methods together: a literal with an elision, an assignment far past the end, the in operator against a hole, forEach recording which indices it visited, map over a sparse array, join, truncation by assigning to length, the default lexicographic sort, a numeric comparator, a stability check over four records with two distinct keys, mutating versus non-mutating operations, splice return values, reduce with and without an initial value, Array.from, Array.of, Array.isArray, an array-like built from arguments, the difference between indexOf and includes for NaN, flat at an explicit depth, flatMap, fill, and a JSON round trip.

It was run in Node, protected, and run again on the default ES5 target, the modern target, a renaming build, that build on the modern target, and a string-table build. All five matched the original.

The results that matter most: the sparse array still reported a length of 11 with 1 in sparse false, so the hole was still a hole and had not been filled with undefined. forEach still visited only indices 0, 2 and 10. join still produced 1--3 with nothing between the separators. The stability check still produced bdac, preserving the input order within each key group.

Holes survive as holes

The distinction between a hole and an undefined value is the one most likely to be lost by a transform that normalises array literals, so it is worth separating the measured behaviours.

forEach and the other callback methods skip holes but visit positions holding undefined. map preserves holes in its result rather than calling the callback for them, which is why the measured output showed nulls in the JSON rendering at exactly the untouched positions. join renders both a hole and undefined as empty text, so it does not distinguish them, while the in operator does. All of these agreed before and after protection.

Assigning to length still truncated in place, discarding the elements past the new length rather than leaving them reachable. That is the other half of array shape being genuine runtime state rather than a rendering of the literal you typed.

Sorting: stability and the lexicographic default

Two sort results came through unchanged, and they pull in opposite directions in terms of what surprises people.

The default sort converts elements to strings and compares them, which is why [10, 9, 1, 100] sorts to 1, 10, 100, 9 rather than in numeric order. That is correct behaviour, it is a frequent source of bugs in ordinary code, and it is unchanged by protection. If a protected build appears to sort numbers wrongly, the comparator was missing before protection too.

Stability is the more consequential guarantee. Four records with two distinct sort keys came out in the same relative order within each group after protection, matching the original exactly. Comparator functions themselves are ordinary functions and get renamed like anything else, which changes their names in a stack trace and nothing about the ordering they produce.

Why array shape is out of reach of the transforms

The structural reason is that almost none of this is syntax. An array's length, which indices are present, and the ordering an engine produces from a comparator are runtime state of a value, not properties of the text that created it. The transforms operate on identifiers, literals and control flow, and they have no representation in which the shape of a live array is something that could be edited.

The one place where syntax is genuinely involved is the elision in an array literal, the empty slot in [1, , 3]. That is a real piece of syntax a writer could plausibly normalise into an explicit undefined, which would silently convert a hole into a value. Measured, it is emitted with the elision intact: 1 in sparse was still false in the protected build, on every configuration.

This article covers arrays specifically. How a custom iterable behaves when it is consumed is a separate question that turns on the consumer rather than on array shape, and it is not covered here.

As always the cheap check is your own suite against a protected build. Sorting and list rendering are usually well covered by existing tests, and if you have a snapshot test over a rendered table you already have the strongest version of this check.

Frequently asked questions

Do array holes survive protection?

Yes. A sparse array still reported the same length with the in operator returning false for a hole, so holes were not filled with undefined. forEach still skipped them, map still preserved them, and join still rendered them as empty text, on all five configurations measured.

Is Array.prototype.sort still stable after protection?

Yes. Four records with two distinct sort keys came out in the same relative order within each group before and after protection. Comparator functions are renamed like any other function, which changes their names in a stack trace and nothing about the ordering.

Why does my protected build sort numbers in the wrong order?

Because the default sort converts elements to strings and compares them, so [10, 9, 1, 100] becomes 1, 10, 100, 9. That is unchanged by protection and happens in the unprotected build too. Pass a numeric comparator to sort.

Does assigning to length still truncate an array?

Yes. Setting length to a smaller value still discarded the elements beyond it in place, measured identically in both builds.

Does the ES5 target change how array literals are emitted?

Not in a way that changes behaviour. The one syntactic risk is the elision in a literal such as [1, , 3], which a writer could normalise into an explicit undefined and thereby turn a hole into a value. Measured, the elision is emitted intact on every configuration tested.

What is the cheapest way to check this against my own code?

Run your existing test suite against a protected build using your project's configuration. Sorting and list rendering are usually already covered, and a snapshot test over a rendered table is the strongest version of the check.

Related reading