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)