I set out to share a custom sorting function of an array by the order of another array. I'd used this function in a recent project, and it was pretty nifty. But I found myself having to dig deeper into the guts of the JavaScript native sort() array method in order to make what I was writing make sense.

That function I'm going to share in a sequel. But I thought this got to come first - understanding sort() in JavaScript - so that we could have it as an anchor to reference anytime we're trying to understand any custom sorting function we may write.

What is sort()?

sort() is a native array method that takes an array, converts its elements into strings, and re-orders them according to their character's Unicode point value. Afterward, sort() returns the "sorted" array.

Since sort() does not operate on a copy of the given array, the original array is mutated or lost. As you work with this method, you might learn of ways to run it on a copy of an array instead.

Consider two examples of sort() in action:

    var
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
        numbers = [1, 300, 4, 67, 8, 9, 10]

    console.log(months.sort()) // [ "Apr", "Feb", "Jan", "Jun", "Mar", "May" ]
    console.log(numbers.sort()) // [ 1, 10, 300, 4, 67, 8, 9 ]

    console.log(months) // [ "Apr", "Feb", "Jan", "Jun", "Mar", "May" ]
    console.log(numbers) // [ 1, 10, 300, 4, 67, 8, 9 ]

    // Notice that the original arrays are gone - forever.

You can easily tell from those examples the sequence JavaScript "might" be following to order items in the array. Yes, "might" because the verdict is still somewhat muddy to me here. Alphabetical? Numerical? Well,... Like in our definition, items are re-arranged according as their characters appear (or are weighted) in the Unicode system of code units. So, even if 9 comes before 10 in the numeric system, 10's string's Unicode character precedes that of 9.

Nonetheless, please, let's leave the topic of Unicode and JavaScript Unicode to another writing, okay? So, you can for now rest assured that as explained is how JS is doing the sorting.

Now, this default ordering might upset you. I mean, why, for example, should April come before January? You might need to define how you want your array to be sorted. Of course. It's why we got an optional, let's call it, sorter() function to pass in as argument to sort to override default.

There's but a few things to know about and do with sorter(), which, as soon as you nail those, you should've understood sort() and can work easily with the method. Let's see about those.

What you need to understand sort()

Here's how you write the sorter() function:

    var sorter = (a, b) => a < b ? -1 : 1

What you're saying there is that the function should compare each pair of elements and return either of -1 or +1 depending on the size or order of the element. The question to ask is, why -1 or 1? Knowing the answer is what I needed to nail sort().

If you're familiar with math, that's like a number-line comparison, where a negative number is less than a positive one and precedes the latter. Really, its a function like our sorter that JavaScript is using to sort the array after converting to UTF-16 data type.

    var
        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
        numbers = [1, 300, 4, 67, 8, 9, 10]

    console.log(months.sort(sorter)) // [ "Apr", "Feb", "Jan", "Jun", "Mar", "May" ]
    console.log(numbers.sort(sorter)) // [ 1, 4, 8, 9, 10, 67, 300 ]

Our sorter() works predictably when the array elements are real numbers. Now, JavaScript doesn't have to do any conversion to string and Unicode, but simply run sorter() and do as dictated.

Now, with string data type, it kind of works, but alphabetically - April still coming before January. But knowing what is going on can help us get to whatever result we desire.

Incidentally, to have a random list of months sorted into the Jan - Dec order that we're used to, we'd have to define that order and instruct JS to sort according to ours. That? Yup, another post.

So, let's get to behind the scenes of sort() - we've kind of left this off too long...

  1. sort mutates the original array. It sorts the elements in the array, not a copy of it, and returns the sorted array, destroying the orignal. So, if you want to preserve the original array, sort a copy of it instead. Do something like:

        // Spread the numbers array into a copy of itself before sorting
        [...numbers].sort(sorter)
  2. It is the return value of the sorter() function, which is more or less called compareFunction(), that determines how a or b, that is, element1 or element2, is compared. Why an element pair?
  3. sort starts its comparison in order to sort by picking the first element (index 0) of the array, which it assigns to a and the second element (index 1), which it assigns to b. Then it runs sorter() on the pair. The next iteration, b becomes a and the next element is picked as b. And so on...

        // Testing how sort picks elements
        var numbers = [1, 300, 4, 67, 8, 9, 10]
    
        numbers.sort((a, b) => console.log(a, b))
    
        // You'll have a numbers.length - 1 number of loops/comparisons - 7 items in the array, so 6 loops
        /**
            1 300
            300 4
            4 67
            67 8
            8 9
            9 10
        */
  4. I imagine that JavaScript is playing a little game here where it might temporarily hold in memory any element it has already sorted, which might compare as smaller than any previously sorted element, so that it would push that one far behind any of its bigger-than, if you get what I mean. Otherwise, wouldn't you expect that JS would run an endless loop making sure the order is as expected?

    I mean, look at it, say you had 250 right after 10 in the numbers array. Then your last commparison loop, should be 10 250, right? Now, 10 is less than 250, so sorter returns -1 and 10...ooo, lemme back up right there, because what I'm thinking is, since JS is sorting the array, re-arranging it, by the time it gets to 10, that is, whatever is at that index at that time, it may no longer be 10. So, my thinking of thinking a number higher might precede a lower one depending on the length of the array, should be flawed...or some...I shouldn't confuse you any further if you get the idea. 😊

  5. If the result of sorter is less than 0 (kind of like -1), the instance of a is sorted to an index lower than that of b, meaning, it'll be placed before in the array.
  6. As in 5, if the result of sorter is greater than 0 (kind of like 1), the instance of a is sorted to an index higher than that of b, meaning, it'll be placed after b in the array.
  7. And, yet, if the result of sorter is equal to 0, the positions of a and b are unaltered.
  8. undefined elements are all pushed to the end of the sorted array.
  9. When the array you're sorting is strictly numerical numbers, you can write sorter like so:

        // A more succinct sorter for numbers
        var sorter = (a, b) => a - b

Is it making sense, though? Why do I feel I've babied the explanation a bit too much?

Let me close by saying

I don't want to leave off without sharing a line from the MDN doc that struck me. Which line, kind of backs the point I was trying hard to make in #4 of my list up there of the things you need to know of sort.

... but sorted with respect to all different elements.

Look at that! The full quote is:

If compareFunction(a, b) returns 0, leave a and b unchanged with respect to each other, but sorted with respect to all different elements.

So, past "touched" items might not just be "left alone" like that, you know. They're kind of held up again against newer comparisons, just so that, in case, they either smaller or bigger in value, they might get switched around, if you get what I mean. I don't know if this is the best way to explain it, but I hope you get the idea. Or you can share how you might tell it in the comments. Cheers!