Sorting Arrays Correctly
Array.sort mutates, default string order breaks numbers — stable sort, compare functions, localeCompare, and immutable patterns.
- javascript
- sorting-arrays
Array.prototype.sort is one of the most footgun-heavy APIs in JS interviews. Default compare converts to strings. It mutates in place. Modern engines use a stable sort, but you should still write an explicit comparator for anything non-trivial.
[10, 2, 1].sort();
// [1, 10, 2] — lexicographic: '10' < '2'
Numeric and general compare
nums.sort((a, b) => a - b); // ascending
nums.sort((a, b) => b - a); // descending
// comparator contract:
// < 0 → a before b
// > 0 → a after b
// 0 → keep relative order (stable)
Don’t return booleans (a > b) — coerced to 0/1, wrong for ascending.
Strings and locale
names.sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));
// numeric option for dotted versions / house numbers
['item2', 'item10', 'item1'].sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true }),
);
// item1, item2, item10
Objects
users.sort((a, b) => a.lastName.localeCompare(b.lastName)
|| a.firstName.localeCompare(b.firstName));
Multi-key: first non-zero comparison wins.
Immutable sort (React state)
// BAD — mutates existing array reference’s contents
state.items.sort(...);
// GOOD
const sorted = [...state.items].sort((a, b) => a.price - b.price);
setState({ ...state, items: sorted });
// modern
const sorted2 = state.items.toSorted((a, b) => a.price - b.price);
toSorted / toReversed (ES2023) return copies; check Baseline if you skip polyfills.
Stability
const rows = [
{ name: 'b', order: 1 },
{ name: 'a', order: 2 },
{ name: 'b', order: 3 },
];
rows.sort((a, b) => a.name.localeCompare(b.name));
// both 'b' rows keep relative order (order 1 then 3) on modern engines
Stability is required by the ECMAScript spec now; still don’t rely on old IE myths in interviews — mention it briefly.
Mixed types and NaN
[1, NaN, 2].sort((a, b) => a - b); // NaN comparisons are messy
Normalize data before sort: filter nulls, coerce prices with Number, decide where empties go.
function comparePrice(a, b) {
const pa = a.price ?? Infinity;
const pb = b.price ?? Infinity;
return pa - pb;
}
Interview answer (out loud)
“Default sort is string order and mutates the array. For numbers I use (a,b)=>a-b; for strings localeCompare; for immutable UI state I copy first or use toSorted. Comparators return negative, zero, or positive — not booleans. Modern JS sort is stable.”
Schwartian transform for expensive keys
function sortByExpensive(arr, keyFn) {
return arr
.map((item, i) => ({ item, i, k: keyFn(item) }))
.sort((a, b) => (a.k < b.k ? -1 : a.k > b.k ? 1 : a.i - b.i))
.map(({ item }) => item);
}
Compute the sort key once per element when keyFn is heavy (normalize strings, parse dates).
Dates and ISO strings
events.sort((a, b) => Date.parse(a.iso) - Date.parse(b.iso));
// or store epoch ms numbers to avoid reparse
Invalid dates yield NaN comparisons — filter or push invalids to the end explicitly.
Locale-aware names
names.sort((a, b) => a.localeCompare(b, navigator.language));
Hard-coding 'en' may be wrong for international products; pass the active UI locale.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.