Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JS - Why array.sort() doesn't return the same result for objects and numbers?

Sorting an array of objects (by a property of type number) doesn't return a sorted result like for an array of numbers.
Why so ?
How to make it sort like for numbers ?

Demo: Sorting an array of numbers

const sorted = [0,  5,  2, undefined,  3,  1,  4]
  .sort((a, b) => a - b);
  
console.log(sorted);

Demo: Sorting an array of objects

const notSorted = [
  {i:0},
  {i:5},
  {i:2},
  {i: undefined},
  {i:3},
  {i:1},
  {i:4},
]
  .sort((a, b) => a.i - b.i)
  .map(a => a.i);
  
console.log(notSorted);

I am currently on Chrome 90. Maybe some other browsers or engines don't have this issue. Tell me.

like image 631
Yairopro Avatar asked May 06 '21 14:05

Yairopro


People also ask

Why is sort not working properly JavaScript?

This is because JavaScript's sort() method converts each item in the array into strings and constructs the sequence by comparing each array item based on the UTF-16 code values when there is no callback specified.

Does sort work with objects?

To sort an array of objects, use the sort() method with a compare function. A compareFunction applies rules to sort arrays by defined our own logic. They allow us to sort arrays of objects by strings, integers, dates, or any other custom property.

Can sort be applied to array of objects?

To sort an array of objects, you use the sort() method and provide a comparison function that determines the order of objects.


Video Answer


3 Answers

According to the spec:

  • If x and y are both undefined, return +0.
  • If x is undefined, return 1.
  • If y is undefined, return −1.
  • If the argument comparefn is not undefined, then
    • Let v be ToNumber(Call(comparefn, undefined, «x, y»)).
    • ReturnIfAbrupt(v).
    • If v is NaN, return +0.
    • Return v.

That explains why it works in the first case because the sorted values are not enclosed in an object. In the second case the values are not undefined (only the properties are) so the native undefined handling of Array.prototype.sort() doesn't take over, which means the callback is being executed even if a.i or b.i is undefined, and it returns NaN (not a number).

As the callback returns NaN for every undefined property, they are considered equal to every other item. That leads to an erratic behavior, which depends on the actual algorithm of Array.prototype.sort() in the JavaScript engine.

Here are the return values of the question example for some browsers:

  • IE 11: [0, 1, 2, 5, undefined, 3, 4]
  • Edge Chromium 90: [0, 1, 2, 3, 5, undefined, 4]
  • Firefox 88: [0, 2, 5, undefined, 1, 3, 4]
like image 110
Guerric P Avatar answered Nov 14 '22 22:11

Guerric P


Your sorting algorithm produces, in some instances, NaN, since undefined - someNum and someNum - undefined both result in NaN. This means that your callback is not consistent, which means that the resulting sort order is implementation-defined.

A function comparefn is a consistent comparison function for a set of values S if all of the requirements below are met for all values a, b, and c (possibly the same value) in the set S: The notation a <CF b means comparefn(a, b) < 0; a =CF b means comparefn(a, b) = 0 (of either sign); and a >CF b means comparefn(a, b) > 0.

  • Calling comparefn(a, b) always returns the same value v when given a specific pair of values a and b as its two arguments. Furthermore, Type(v) is Number, and v is not NaN. Note that this implies that exactly one of a <CF b, a =CF b, and a >CF b will be true for a given pair of a and b.

If you ever return NaN from a .sort callback, your results can be anything at all: the behavior in such a case is undefined by the specification (though certain implementations might produce a result that makes more intuitive sense... or not). So, make sure never to return NaN. In this case, explicitly test to see if the .i property being iterated over is undefined, and substitute a different value for it - maybe Infinity or -Infinity.

const sanitize = val => val === undefined ? Infinity : val;

const notSorted = [
  {i:0},
  {i:5},
  {i:2},
  {i: undefined},
  {i:3},
  {i:1},
  {i:4},
]
  .sort((a, b) => sanitize(a.i) - sanitize(b.i))
  .map(a => a.i);
  
console.log(notSorted);
like image 23
CertainPerformance Avatar answered Nov 14 '22 20:11

CertainPerformance


Because you got an object with an undefined property in that array, on which your comparison function is not consistent. You'll need to ensure that it returns a number, not NaN. Chrome uses different algorithms for sorting number arrays vs object arrays, and that you got lucky in one case doesn't mean it would always work. It does work as expected with the array of plain numbers, since .sort() ignores undefined array elements (not attempting to compare them against something else) and always puts them at the end of the array.

You can fix it by doing

.sort((a, b) => (a.i ?? -Infinity) - (b.i ?? -Infinity))

(or +Infinity, depending on whether you want your undefined values first or last).

like image 41
Bergi Avatar answered Nov 14 '22 20:11

Bergi