Sorting an array in JavaScript, a utility perspective!

If you have written some JavaScript and manipulate slightly complex data, you have had to write some code like this to sort an array of objects:

const data = [
  { name: "Alice", age: 22 },
  { name: "Bob", age: 32 },
  { name: "Carl", age: 63 },
  { name: "Clara", age: 28 },
  ...
];

data.sort(function(a, b) {
  if (a.name < b.name) {
    return -1;
  }
  
  if (a.name > b.name) {
    return 1;
  }
  
  return 0;
})

// Or, as a one-liner: 
data.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)

While this is perfectly fine for one-off sorting of shallow objects, it can get a bit more complex and repetitive when having to sort based on nested fields.

Something else you might have tripped on while using the native .sort() on arrays, is the following behaviour:

const array = [1, 2, 3, 10, 23]
console.log(array.sort())
// [1, 10, 2, 23, 3]

Indeed, by default, the comparison function used by .sort() treats each element as a string! To make the above example work, you need to pass a custom comparison function such as the following one-liner:

const array = [1, 23, 3, 10, 2]
console.log(array.sort((a, b) => a - b))
// [1, 2, 3, 10, 23]

As sorting is a common operation on arrays, a more scalable and less error-prone strategy would be to define common compare functions. Let's build said compare functions!

First, let's look at the API we would like to end up with:

const array = [1, 23, 3, 10, 2]
array.sort(numerically)
// Should be equivalent to:
array.sort((a, b) => a - b)

array.sort(numerically.desc)
// Should be equivalent to:
array.sort((a, b) => b - a)
// For completeness, we can also expose `numerically.asc`.

To achieve the above API, we can define numerically as follows:

function numerically (a, b) {
  return a - b;
}

As in JavaScript, (almost) everything is an object, we can then add a desc and an asc field to the numerically function as follows:

numerically.desc = function(a, b) {
  return b - a;
}

numerically.asc = function(a, b) {
  return numerically(a, b); // This works because we sort from lower to higher by default!
}

Now that we have defined compare functions to work on arrays holding primitives values, let's generalise it to arrays of objects:

const data = [
  { name: "Alice", age: 22 },
  { name: "Bob", age: 32 },
  { name: "Carl", age: 63 },
  { name: "Clara", age: 28 },
  ...
];

data.sort(alphabetically.by("name"))
// Should be equivalent to:
data.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)

To achieve that, let's create a small utility function that will help us retrieve the value of an object based on a key path:

function getValueByKey(obj, key) {
  return String(key)
    .split(".")
    .reduce((acc, cur) => acc?.[cur] ?? null, obj);
}

With the code above, we can do deep object look-ups!

With that in hand, let's add the following to our example alphabetically sort function:

function alphabetically (a, b) { ... }

alphabetically.desc = function(a, b) { ... }
alphabetically.asc = function(a, b) { ...}

alphabetically.by = function(key) {
  return function(a, b) {
    const aVal = getValueByKey(a, key);
	const bVal = getValueByKey(b, key);
	
	return a < b ? -1 : a > b ? 1 : 0;
  }
}

Alright, this works great for ascending order sorting, but how could we implement descending order? There are different ways of solving this:

  • Pass another argument that can have either "desc" or"asc" values (defaults to "asc")
  • Append a - sign in the key (for example: sort(alphabetically.by("-name"))
  • Add .desc() and .asc() functions to our new function .by()

Either designs are fine, but to stay consistent with our previous utility function, we will be adding the ordering feature as follow:

data.sort(alphabetically.by("name").desc)

All implemented, it looks like:

function alphabetically (a, b, direction = 1) {
  if (a < b) {
    return -1 * direction;
  }
  
  if (a > b) {
    return 1 * direction;
  }
  
  return 0;
}

alphabetically.asc = (a, b) => alphabetically(a, b, 1);
alphabetically.desc = (a, b) => alphabetically(a, b, -1);

alphabetically.by = function(key) {
  function compareBy(a, b, direction = 1) {
    const aVal = getValueByKey(a, key);
	const bVal = getValueByKey(b, key);
	
	return aVal < bVal ? -1 * direction : aVal > bVal ? 1 * direction : 0;
  }
  
  compareBy.asc = (a, b) => compareBy(a, b, 1);
  compareBy.desc = (a, b) => compareBy(a, b, -1);
  
  return compareBy;
}

I found this exercise particularly interesting, and decided to build a library with some of the ideas discussed in this post. You can have a look at it here:

https://github.com/AntonioVdlC/sort

Antonio Villagra De La Cruz

Antonio Villagra De La Cruz

Multicultural software engineer passionate about building products that empower people.