Learn how to extend JavaScript's Array prototype with a powerful and flexible merge sort function that supports nested objects, strings, and numbers.
๐ง Creating a Merge Sort Array Prototype for Nested Objects, Strings, and Numbers
Sorting arrays is easy โ until you need to sort deeply nested objects, ensure stable results, or avoid mutating the original array.
In this post, youโll learn how to create a powerful, non-mutating mergeSortBy() function on the Array.prototype that supports:
- โ Numbers
- โ Strings (case-insensitive)
- โ
Complex nested object keys (e.g.,
"user.address.city") - โ Custom comparator functions
๐ Why Not Just Use .sort()?
| Feature | Array.sort() |
mergeSortBy() (Custom) |
|---|---|---|
| Mutates original array? | โ Yes | โ No |
| Stable sort? | โ Not guaranteed before ES10 | โ Always |
| Deep key sorting? | โ No | โ Yes |
| Supports custom comparator? | โ Yes | โ Yes |
๐ Step 1: Get Nested Values
function getValue(obj, path) {
return path.split('.').reduce((acc, key) => acc?.[key], obj);
}
๐งฎ Step 2: Comparator Helper
function defaultComparator(a, b) {
if (a == null && b != null) return -1;
if (a != null && b == null) return 1;
if (typeof a === 'string' && typeof b === 'string') {
return a.localeCompare(b);
}
return a > b ? 1 : a < b ? -1 : 0;
}
๐งฉ Step 3: mergeSortBy on Array.prototype
if (!Array.prototype.mergeSortBy) {
Array.prototype.mergeSortBy = function (keyOrFn) {
const getComparator = () => {
if (typeof keyOrFn === 'function') return keyOrFn;
if (typeof keyOrFn === 'string') {
return (a, b) =>
defaultComparator(getValue(a, keyOrFn), getValue(b, keyOrFn));
}
return defaultComparator;
};
const merge = (left, right, comparator) => {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
result.push(
comparator(left[i], right[j]) <= 0 ? left[i++] : right[j++]
);
}
return result.concat(left.slice(i), right.slice(j));
};
const mergeSort = (arr, comparator) => {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid), comparator);
const right = mergeSort(arr.slice(mid), comparator);
return merge(left, right, comparator);
};
return mergeSort(this.slice(), getComparator()); // non-mutating
};
}
๐งช Test Cases & Real Examples
โ 1. Sort numbers
[9, 1, 4, 7].mergeSortBy(); // [1, 4, 7, 9]
โ 2. Sort strings
["banana", "Apple", "cherry"].mergeSortBy();
// Output: ['Apple', 'banana', 'cherry']
โ 3. Sort objects with deep keys
const users = [
{ id: 1, profile: { name: "John" } },
{ id: 2, profile: { name: "Alice" } },
{ id: 3, profile: { name: "Bob" } },
];
const sortedUsers = users.mergeSortBy("profile.name");
console.log(sortedUsers.map(u => u.profile.name));
// ['Alice', 'Bob', 'John']
โ 4. Custom comparator
const scores = [
{ name: "A", score: 90 },
{ name: "B", score: 100 },
{ name: "C", score: 95 },
];
const desc = scores.mergeSortBy((a, b) => b.score - a.score);
console.log(desc.map(x => x.score)); // [100, 95, 90]
โฑ Performance Comparison: .sort() vs mergeSortBy()
const bigArray = Array.from({ length: 100000 }, () => ({
id: Math.random().toString(36).substring(7),
value: Math.floor(Math.random() * 100)
}));
console.time("Native sort");
bigArray.slice().sort((a, b) => a.value - b.value);
console.timeEnd("Native sort");
console.time("mergeSortBy");
bigArray.mergeSortBy("value");
console.timeEnd("mergeSortBy");
โ Stability Test
const people = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 30 },
];
const sorted = people.mergeSortBy("age");
console.log(sorted.map(p => p.name));
// ['Bob', 'Alice', 'Charlie'] โ Alice before Charlie (same age) โ stable!
๐ฏ Final Takeaways
You just built a custom merge sort that:
- Handles primitives and nested objects
- Accepts string paths or comparator functions
- Is stable and predictable
โ ๏ธ Performance: Why Native .sort() Is Faster Than mergeSort()
After testing both Array.prototype.sort() and our custom mergeSort() on complex nested data, the results are clear:
Native Sort: โ
Faster (~1.3ms)
Merge Sort: โ Slower (~8.5ms)
๐ Why is Native .sort() Faster?
| Reason | Description |
|---|---|
| Engine Optimized | Native .sort() is implemented in low-level languages like C++ inside JS engines (e.g., V8). |
| Hybrid Sorting | Modern engines use Timsort โ a hybrid of Merge and Insertion sort โ optimized for real-world data. |
| Better Memory Handling | Avoids frequent array cloning operations like slice() and shift(), which are costly in JavaScript. |
| In-place Sorting | Operates directly on the original array without creating new intermediate arrays. |
| Small-Array Optimization | For short arrays, engines switch to insertion sort, which is extremely fast. |
๐ Then Why Bother With mergeSort()?
While native sort is faster, writing your own mergeSort() still has value:
- โ Educational: Great way to learn recursion and sorting logic.
- ๐ Transparent: Full control over how elements are compared and sorted.
- ๐ Immutable Structures: Useful in environments where mutation is discouraged.
- ๐ Benchmarking: Compare against other algorithms (QuickSort, HeapSort, etc.) for algorithm study.
๐ง Takeaway
Use
.sort()in production.
UsemergeSort()to understand how sorting works under the hood.
๐ฌ What Next?
Let me know:
- Would you like a TypeScript version?
- Should this be a standalone NPM utility?
โญ Found this helpful? Drop a like, comment, or bookmark!

Top comments (0)