Out of the box JavaScript Array.prototype.sort method uses simple yet effective approach — it converts every item to Unicode string and orders them by comparing respective code point values. That explains, why [1, 2, 10].sort() produces [1, 10, 2], instead of more logical [1, 2, 10]. If you want more, e.g. sort in descending order or by object property name, Array.prototype.sort accepts custom comparer function that allows you to implement your own sorting logic.
The following example demonstrates how to sort array of objects by name in descending order:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var users = [{ name: "John", age: 30}, { name: "Alex", age: 35 }, { name: "Simon", age: 22 }, { name: "Alex", age: 40}]; var byNameDescending = function (a, b) { if (a.name > b.name) { return -1;//or +1 for ascending order } else if (a.name < b.name) { return 1;//or -1 for ascending order } else { return 0; } }; users.sort(byNameDescending);//Simon, John, Alex, Alex |
Custom comparer takes two arguments — two items to compare — and returns anything less than zero, if first argument should come before the second, anything greater than zero, if first argument should come after the second, and zero, if arguments considered equal.
So far, so good. What if I want to compare by two fields, e.g. name and age? No problem, just add another comparer function, let’s say byAgeDescending, and when comparing by name produces zero, return byAgeDescending comparer value instead.
Ok, and what if I decided to change sort direction? Again, no problem, move +/-1 to local variable and make its value to depend on sort direction.
Actually, there is a problem. This sorting functions are not very easy to understand and they are designed for this specific scenario. If I ever going to sort by last name and gender, I have to create new comparers.
This, on the other hand, is a little bit easier to follow and can be used in multiple places:
1 2 |
var by = function (rules) {...}; arr.sort(by("name DESC, age")) |
Function named by takes SQL-like sorting rule and constructs new function, which can compare any 2 objects by such rule. This isn’t hard to implement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
var by = function (rules) { var comparers = rules.trim().split(",").map(function (rule) { var ruleComponents = rule.trim().split(" "), fieldName = ruleComponents[0], isAscending = ruleComponents.length < 2 || ruleComponents[1].toUpperCase() !== "DESC", k = isAscending ? 1 : -1; return function (a, b) { var val1 = a[fieldName], val2 = b[fieldName]; if (val1 > val2) { return k; } else if (val1 < val2) { return -k; } else { return 0; } }; }); return function (a, b) { var result, i = 0, comparer = comparers[i]; do { result = comparer(a, b); comparer = comparers[++i]; } while (result === 0 && comparer) return result; }; }; |
It first converts comma separated list of sorting rules into individual comparers, one per field, and then returns master comparer, which will try per-field comparers one by one, until someone clearly says which item is greater, and which is lower. The beauty of this approach is it’s highly reusable and really easy to understand.