Sorting JSON arrays is essential for organizing data for display, analysis, and comparison. JavaScript's Array.prototype.sort() is deceptively simple—its default string-based comparison produces unintuitive results for numbers, and sorting deeply nested object arrays requires understanding compare functions, stability guarantees, and locale-aware string ordering.
How JavaScript sort() Actually Works
Array.prototype.sort() converts elements to strings and compares their UTF-16 code unit sequences by default. This is why [3, 1, 10, 2].sort() produces [1, 10, 2, 3]—"10" comes before "2" lexicographically. Always provide a compare function for numeric sorting. The compare function should return a negative value if a should come before b, positive if b should come before a, and zero if they are equal. Modern JavaScript engines use TimSort (a hybrid of merge sort and insertion sort), which is stable—elements that compare as equal maintain their original relative order.
Sorting Primitives
// Numeric sort — ALWAYS provide a compare function
[3, 1, 4, 1, 5].sort((a, b) => a - b);
// [1, 1, 3, 4, 5]
// Locale-aware string sort
['café', 'apple', 'Banana'].sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
// ['apple', 'Banana', 'café'] — case-insensitive, accent-aware
Sorting Arrays of Objects by Single Field
const users = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
{ name: 'Carol', age: 35 }
];
users.sort((a, b) => a.age - b.age); // ascending by age
users.sort((a, b) => b.age - a.age); // descending by age
users.sort((a, b) => a.name.localeCompare(b.name)); // alphabetical by name
Multi-Field Sorting
Chain comparisons with the OR operator (||). Each comparison runs only if the previous ones tie:
// Sort by department, then by name within each department
employees.sort((a, b) =>
a.department.localeCompare(b.department) || a.name.localeCompare(b.name)
);
// Sort by priority (desc), then by creation date (asc) for tie-breaking
tasks.sort((a, b) =>
b.priority - a.priority || new Date(a.created) - new Date(b.created)
);
// Sort by status order, then by timestamp
const STATUS_ORDER = { 'active': 0, 'pending': 1, 'archived': 2 };
records.sort((a, b) =>
STATUS_ORDER[a.status] - STATUS_ORDER[b.status] ||
new Date(a.updatedAt) - new Date(b.updatedAt)
);
Sorting by Date, Nested, and Optional Fields
// ISO date strings — construct Date objects in compare function
events.sort((a, b) => new Date(a.date) - new Date(b.date));
// Nested fields with null safety
products.sort((a, b) => {
const priceA = a.specs?.price ?? Infinity;
const priceB = b.specs?.price ?? Infinity;
return priceA - priceB;
});
// Custom sort orders with lookup maps
const PRIORITY = { 'critical': 0, 'high': 1, 'medium': 2, 'low': 3 };
tickets.sort((a, b) => PRIORITY[a.severity] - PRIORITY[b.severity]);
Performance: toSorted vs sort and Large Arrays
sort() mutates the original array. Use toSorted() (ES2023) to return a new sorted array without modifying the original. Both have O(n log n) time complexity with V8's TimSort implementation, but toSorted() adds O(n) memory overhead for the copy. For arrays under 5,000 items, the performance difference is negligible. For larger datasets, prefer sort() if you don't need the original order. For extremely large datasets (100K+), consider pre-extracting sort keys with map() before sorting to avoid repeated property access inside the compare function—this Schwartzian transform can reduce sort time by 40-60%.
Sorting JSON Object Keys for Deterministic Output
JavaScript object property order is: integer-like string keys in ascending numeric order, then remaining string keys in insertion order. For deterministic JSON output (consistent hashing, reproducible diffs), reconstruct objects with sorted keys:
function sortObjectKeys(obj) {
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
if (typeof obj !== 'object' || obj === null) return obj;
return Object.keys(obj).sort().reduce((sorted, key) => {
sorted[key] = sortObjectKeys(obj[key]);
return sorted;
}, {});
}
// JSON.stringify(sortObjectKeys(data), null, 2)