If you want to look behind the curtain an analyze this through the lens of the particular sorting algorithm being used, you can't. The ECMAscript standard does not specify which sort algorithm browsers have to use for JavaScript's sort algorithm, so Firefox might do it one way while Chrome does it another. And then Chrome might change to do it differently later down the line. We just have to accept that level of abstraction.
Now, the are things that we do know. JavaScript allows us to provide a "comparison" function to be used when sorting arrays. you can pass two arguments into that comparison function and return a result to signal which value should appear first in the resulting array.
- Returning a negative number signals that the first argument should occur earlier in the sorted array.
- Returning a positive number signals that the second argument should occur earlier in the sorted array.
- Returning zero signals that the two values are the same and should appear next to each other in the sorted array.
Here's a little snippet to exemplify these rules:
var arr = ["w", "l", "w"];
arr.sort(function(a, b){
if(a === b) return 0; //they're the same
if(a === "w") return -1; //w should appear earlier
return 1; //l should appear later
});
console.log(arr);
We can change this to work with numbers as well:
var arr = [1, 3, 1];
arr.sort(function(a, b){
if(a === b) return 0; //they're the same
if(a < b) return -1; //lower numbers should appear earlier
return 1; //higher numbers should appear later
});
console.log(arr);
So at this point, an interesting question is "What happens if I just always return a positive number?" Well, what you're really telling the sorting algorithm by doing that is that everything should appear later in the sorted array. But what is "later" without anything earlier?! Is it just going to keep looping forever? will my computer explode?
The answer is, that totally depends on the sorting algorithm that the browser chooses to use. You can pretty well trust that the browser's sorting algorithm is going to pick a place to stop sorting and call it a day, but where the chips lie at that time can vary based on the browser. Try this example in Chrome and Firefox, and you'll probably get different results:
var arr = [8, 6, 7, 5, 3, 0, 9];
arr.sort((a, b) => 1);
console.log(arr);
And because any positive number returned in a comparison function signals the same thing, always returning Math.random() will have the same effect (because you can pretty much guarantee that random number will be greater than zero):
var arr = [8, 6, 7, 5, 3, 0, 9];
arr.sort((a, b) => Math.random());
console.log(arr);
It's only when you introduce the possibility of returning a negative number that you starting telling the sorting algorithm to show some values earlier than others in the sorted array:
var arr = [8, 6, 7, 5, 3, 0, 9];
arr.sort((a, b) => Math.random() - .5);
console.log(arr);
As for why we choose .5 specifically? Well, Math.random gives you a number 0 to 1. That's a positive number roughly 100% of the time. If we subtract .1 from Math.random, we start getting results from -0.1 to 0.9. That's a positive number roughly 90% of the time! It would work, but ideally, we want it to be more of a coin flip so we can be happier with the fairness of the random sort. That's why we subtract 0.5 specifically- to get numbers from -0.5 to 0.5, yielding a positive result roughly 50% of the time and a negative result for roughly the other 50% of the time.
But if you care about the quality of the results...
As others have mentioned, the above comparison function has been thoroughly tested and is known to favor some numbers over others. The most popular correct shuffle is called the Fisher Yates shuffle, and you can read about it here. It's definitely not as concise, and when used with Math.random, it's of course not cryptographically secure.
If you need a cryptographically strong random sort but still want something short and sweet, I always recommend having a look at rando.js. You can get a cryptographically strong sort just like this:
console.log(randoSequence([8, 6, 7, 5, 3, 0, 9]));
<script src="https://randojs.com/2.0.0.js"></script>
It keeps track of the original indices by default in case two values are the same but you still care where they came from. If you dont like that, you whittle the result down to just the values with a simple map:
console.log(randoSequence([8, 6, 7, 5, 3, 0, 9]).map((i) => i.value));
<script src="https://randojs.com/2.0.0.js"></script>