Array.sort takes a comparator. One field is straightforward:

data.sort(function(a, b) {
  return a.SCORE - b.SCORE;
});

Multiple fields gets ugly fast. Sort by score descending, then time ascending, then age ascending – you end up chaining comparisons:

data.sort(function(a, b) {
  return (b.SCORE - a.SCORE) || (a.TIME - b.TIME) || (a.AGE - b.AGE);
});

This works for numbers but breaks down with strings, custom transforms, or when you want to configure the sort dynamically. I wanted something reusable.

The pattern below is adapted from a Stack Overflow answer I kept coming back to – writing it up here for my own reference.

The predicate function

function predicate() {
  var fields = [];
  var n_fields = arguments.length;
  var field, name, cmp;

  var default_cmp = function(a, b) {
    if (a === b) return 0;
    return a < b ? -1 : 1;
  };

  var getCmpFunc = function(primer, reverse) {
    var dfc = default_cmp;
    var cmp = default_cmp;
    if (primer) {
      cmp = function(a, b) {
        return dfc(primer(a), primer(b));
      };
    }
    if (reverse) {
      return function(a, b) {
        return -1 * cmp(a, b);
      };
    }
    return cmp;
  };

  for (var i = 0; i < n_fields; i++) {
    field = arguments[i];
    if (typeof field === 'string') {
      name = field;
      cmp = default_cmp;
    } else {
      name = field.name;
      cmp = getCmpFunc(field.primer, field.reverse);
    }
    fields.push({ name: name, cmp: cmp });
  }

  return function(A, B) {
    var result;
    for (var i = 0; i < n_fields; i++) {
      result = fields[i].cmp(A[fields[i].name], B[fields[i].name]);
      if (result !== 0) break;
    }
    return result;
  };
}

Pass it field names as strings for ascending sort, or objects with name, reverse, and primer for more control. It returns a comparator function that Array.sort can use directly.

Usage

data.sort(predicate(
  { name: 'SCORE', reverse: true },
  'TIME',
  'AGE'
));

Score descending, then time ascending, then age ascending. The output:

jane   4000  35  16
yuri   3000  34  19
anita  2500  32  17
joe    2500  33  18
sally  2000  30  16
mark   2000  30  18
bob    2000  32  16
amy    1500  29  19
mary   1500  31  19
tim    1000  30  17

Ties on score fall through to time. Ties on time fall through to age. The comparator short-circuits on the first field that differs.

The primer option handles type coercion – pass parseInt to sort string numbers correctly, or a custom function to normalize values before comparison.

data.sort(predicate(
  { name: 'PRICE', primer: parseInt, reverse: true },
  'NAME'
));

The function preprocesses the field specifications once and returns a closure. The sort comparisons don’t rebuild anything per call – just iterate the fields and break on the first difference.