下划线:基于多个属性进行sortBy()排序

121

我正在尝试根据多个属性对对象数组进行排序。例如,如果两个对象之间的第一个属性相同,则应使用第二个属性来比较这两个对象。例如,考虑以下数组:

var patients = [
             [{name: 'John', roomNumber: 1, bedNumber: 1}],
             [{name: 'Lisa', roomNumber: 1, bedNumber: 2}],
             [{name: 'Chris', roomNumber: 2, bedNumber: 1}],
             [{name: 'Omar', roomNumber: 3, bedNumber: 1}]
               ];

按照 roomNumber 属性排序,我会使用以下代码:

var sortedArray = _.sortBy(patients, function(patient) {
    return patient[0].roomNumber;
});

这个方法可以正常工作,但我该如何进行操作,以便 'John' 和 'Lisa' 能够正确排序?

12个回答

262

sortBy 声明它是一个稳定的排序算法,因此您应该能够先按第二个属性进行排序,然后再按第一个属性进行排序,就像这样:

sortBy声称它是一种稳定的排序算法,所以你可以首先按照第二个属性进行排序,然后再按照第一个属性进行排序,例如:

var sortedArray = _(patients).chain().sortBy(function(patient) {
    return patient[0].name;
}).sortBy(function(patient) {
    return patient[0].roomNumber;
}).value();

当第二个 sortBy 发现约翰和丽莎有相同的房间号时,它将保持它们最初的顺序,即第一个 sortBy 的顺序为 "丽莎,约翰"。


12
这里有一篇博客文章,详细介绍了如何使用Underscore对多个属性进行升序或降序排序的有效信息。 - Alex C
5
这里有一个更简单的解决多关键字排序的方法,您可以在 这里 找到。公平地说,看起来这篇博客文章是在这些答案之后编写的,但是在我尝试使用上面答案中的代码失败后,它帮助我弄清楚了。 - Mike Devenney
1
你确定patient[0].name和patient[1].roomNumber应该有索引吗?patient不是一个数组... - StinkyCat
[0] 索引器是必需的,因为在原始示例中,patients 是一个数组的数组。这也是为什么在另一条评论中提到的博客文章中的“更简单的解决方案”在这里行不通的原因。 - Rory MacLeod
1
@ac_fire 这是一个已经失效链接的存档:http://archive.is/tiatQ - blueberry_chopsticks
显示剩余2条评论

53

以下是我有时在这种情况下使用的技巧:将属性组合在一起,使结果可以排序:

var sortedArray = _.sortBy(patients, function(patient) {
  return [patient[0].roomNumber, patient[0].name].join("_");
});

但是,正如我所说的那样,这种方法比较粗糙。要正确地做到这一点,您可能需要实际使用核心JavaScriptsort方法

patients.sort(function(x, y) {
  var roomX = x[0].roomNumber;
  var roomY = y[0].roomNumber;
  if (roomX !== roomY) {
    return compare(roomX, roomY);
  }
  return compare(x[0].name, y[0].name);
});

// General comparison function for convenience
function compare(x, y) {
  if (x === y) {
    return 0;
  }
  return x > y ? 1 : -1;
}

当然,这个方法会直接就地排序您的数组。如果您想要一个排序后的副本(与_.sortBy相似),请先克隆该数组:

当然,这个方法会直接就地排序您的数组。如果您想要一个排序后的副本(与_.sortBy相似),请先克隆该数组:
function sortOutOfPlace(sequence, sorter) {
  var copy = _.clone(sequence);
  copy.sort(sorter);
  return copy;
}

无聊之余,我刚刚写了一个通用解决方案(可以按任意数量的键排序):看一下


非常感谢这个解决方案,最终我使用了第二个,因为我的属性既可以是字符串也可以是数字。所以似乎没有一种简单的本地方法来对数组进行排序? - Christian R
3
为什么只有 return [patient[0].roomNumber, patient[0].name]; 而没有 join 不足够呢? - Csaba Toth
1
您的通用解决方案链接似乎已经失效了(或者可能是我无法通过代理服务器访问它)。您能否在这里发布一下链接吗? - Zev Spitz
另外,compare函数如何处理非原始值的情况——例如undefinednull或普通对象? - Zev Spitz
请注意,此技巧仅在确保数组中所有项目的字符串长度相同时才有效。 - miex

37

我知道我来晚了,但是我想为那些需要更清洁和更快速的解决方案的人添加这个,而不是已经建议过的。您可以按照最不重要的属性到最重要的属性的顺序链接sortBy调用。在下面的代码中,我创建了一个按原始数组患者房间号姓名排序的新患者数组。

var sortedPatients = _.chain(patients)
  .sortBy('Name')
  .sortBy('RoomNumber')
  .value();

4
尽管你迟到了,但仍然是正确的 :) 谢谢! - Allan Jikamu
3
好的,非常干净。 - Jason Turan
第二个排序不会覆盖第一个吗? - Phil
不,这是一种“稳定”的排序算法,它知道将第二个排序应用于第一个排序的基础上进行排序,这意味着它不会干扰第一个排序中不同项的顺序,同时重新排序其中的项以显示第二个排序的结果。 - Mike Devenney

12

顺便问一下,你为病人初始化的方式有点奇怪,不是吗? 为什么不使用_.flatten()将其初始化为真正的对象数组,而不是单个对象的数组数组,也许这是一个打字错误的问题:

var patients = [
        {name: 'Omar', roomNumber: 3, bedNumber: 1},
        {name: 'John', roomNumber: 1, bedNumber: 1},
        {name: 'Chris', roomNumber: 2, bedNumber: 1},
        {name: 'Lisa', roomNumber: 1, bedNumber: 2},
        {name: 'Kiko', roomNumber: 1, bedNumber: 2}
        ];

我以不同的方式对列表进行了排序,并将Kiko添加到Lisa的床上;只是为了好玩,看看会发生什么变化...

var sorted = _(patients).sortBy( 
                    function(patient){
                       return [patient.roomNumber, patient.bedNumber, patient.name];
                    });

检查已排序的内容,你会看到这个。

[
{bedNumber: 1, name: "John", roomNumber: 1}, 
{bedNumber: 2, name: "Kiko", roomNumber: 1}, 
{bedNumber: 2, name: "Lisa", roomNumber: 1}, 
{bedNumber: 1, name: "Chris", roomNumber: 2}, 
{bedNumber: 1, name: "Omar", roomNumber: 3}
]

所以我的答案是:在回调函数中使用数组。这与Dan Tao的答案非常相似,我只是忘记了join(可能是因为我删除了唯一项数组的数组)
如果使用你的数据结构,那么代码会是这样:

var sorted = _(patients).chain()
                        .flatten()
                        .sortBy( function(patient){
                              return [patient.roomNumber, 
                                     patient.bedNumber, 
                                     patient.name];
                        })
                        .value();

而且做一个测试会很有趣...


认真的说,那就是答案。 - Radek Duchoň

8

这些答案都不是用于在排序中使用多个字段的通用方法所理想的。以上的所有方法都是低效的,因为它们要么需要多次对数组进行排序(对于足够大的列表来说,这可能会使事情变得非常缓慢),要么生成大量的垃圾对象,虚拟机需要清除这些对象(最终减缓程序速度)。

以下是一种快速、高效、易于反向排序的解决方案,可以与underscorelodash一起使用,也可以直接与Array.sort一起使用。

最重要的部分是compositeComparator方法,它接受一个比较函数数组并返回一个新的组合比较器函数。

/**
 * Chains a comparator function to another comparator
 * and returns the result of the first comparator, unless
 * the first comparator returns 0, in which case the
 * result of the second comparator is used.
 */
function makeChainedComparator(first, next) {
  return function(a, b) {
    var result = first(a, b);
    if (result !== 0) return result;
    return next(a, b);
  }
}

/**
 * Given an array of comparators, returns a new comparator with
 * descending priority such that
 * the next comparator will only be used if the precending on returned
 * 0 (ie, found the two objects to be equal)
 *
 * Allows multiple sorts to be used simply. For example,
 * sort by column a, then sort by column b, then sort by column c
 */
function compositeComparator(comparators) {
  return comparators.reduceRight(function(memo, comparator) {
    return makeChainedComparator(comparator, memo);
  });
}

您还需要一个比较函数来比较您希望排序的字段。 naturalSort 函数将创建一个比较器以给定特定字段。编写一个用于反向排序的比较器也很简单。

function naturalSort(field) {
  return function(a, b) {
    var c1 = a[field];
    var c2 = b[field];
    if (c1 > c2) return 1;
    if (c1 < c2) return -1;
    return 0;
  }
}

到目前为止,所有的代码都是可重用的,可以放在实用模块中保留。

接下来,您需要创建复合比较器。对于我们的示例,它应该如下所示:

var cmp = compositeComparator([naturalSort('roomNumber'), naturalSort('name')]);

这将按照房间号排序,然后是姓名。添加其他排序条件很简单,并且不会影响排序的性能。
var patients = [
 {name: 'John', roomNumber: 3, bedNumber: 1},
 {name: 'Omar', roomNumber: 2, bedNumber: 1},
 {name: 'Lisa', roomNumber: 2, bedNumber: 2},
 {name: 'Chris', roomNumber: 1, bedNumber: 1},
];

// Sort using the composite
patients.sort(cmp);

console.log(patients);

返回以下内容
[ { name: 'Chris', roomNumber: 1, bedNumber: 1 },
  { name: 'Lisa', roomNumber: 2, bedNumber: 2 },
  { name: 'Omar', roomNumber: 2, bedNumber: 1 },
  { name: 'John', roomNumber: 3, bedNumber: 1 } ]

我喜欢这种方法的原因是它可以快速地在任意数量的字段上进行排序,不会生成大量垃圾或在排序过程中执行字符串连接,并且可以轻松地使用它来使某些列逆序排序,而顺序列则使用自然排序。

5

2

只需返回一个要排序的属性数组:

ES6语法

var sortedArray = _.sortBy(patients, patient => [patient[0].name, patient[1].roomNumber])

ES5 语法

var sortedArray = _.sortBy(patients, function(patient) { 
    return [patient[0].name, patient[1].roomNumber]
})

这不会有将数字转换为字符串的任何副作用。


2

也许underscore.js或JavaScript引擎现在与这些答案撰写时不同,但我能够通过返回一个排序键的数组来解决这个问题。

var input = [];

for (var i = 0; i < 20; ++i) {
  input.push({
    a: Math.round(100 * Math.random()),
    b: Math.round(3 * Math.random())
  })
}

var output = _.sortBy(input, function(o) {
  return [o.b, o.a];
});

// output is now sorted by b ascending, a ascending

请查看这个示例,了解具体操作:https://jsfiddle.net/mikeular/xenu3u91/


1

我认为你最好使用_.orderBy而不是sortBy

_.orderBy(patients, ['name', 'roomNumber'], ['asc', 'desc'])

4
你确定 orderBy 在下划线中吗?我在文档或者我的 .d.ts 文件中找不到它。 - Zachary Dow
1
Underscore 中没有 orderBy。 - AfroMogli
1
_.orderBy 可以使用,但它是 lodash 库的一个方法,而不是 underscore:https://lodash.com/docs/4.17.4#orderBylodash 大多数情况下可以替代 underscore,因此对于 OP 来说可能是合适的选择。 - Mike K

1
我认为大多数答案并不真正有效,当然也没有一种方法可以完全使用下划线同时工作。
这个答案提供了多列排序的功能,并且能够反转其中某些列的排序顺序,所有这些功能都在一个函数中实现。
它还逐步构建最终的代码,因此您可能希望采用最后的代码片段。

我只用它来实现双列布局(首先按 a 排序,然后再按 b 排序):

var array = [{a:1, b:1}, {a:1, b:0}, {a:2, b:2}, {a:1, b:3}];
_.chain(array)
 .groupBy(function(i){ return i.a;})
 .map(function(g){ return _.chain(g).sortBy(function(i){ return i.b;}).value(); })
 .sortBy(function(i){ return i[0].a;})
 .flatten()
 .value();

这是结果:

0: {a: 1, b: 0}
1: {a: 1, b: 1}
2: {a: 1, b: 3}
3: {a: 2, b: 2}

我相信这可以推广到两个以上的情况...


另一个可能更快的版本:
var array = [{a:1, b:1}, {a:1, b:0}, {a:2, b:2}, {a:1, b:3}];
_.chain(array)
    .sortBy(function(i){ return i.a;})
    .reduce(function(prev, i){
        var ix = prev.length - 1;
        if(!prev[ix] || prev[ix][0].a !== i.a) {
         prev.push([]); ix++;
        }
        prev[ix].push(i);
        return prev;
    }, [])
    .map(function(i){ return _.chain(i).sortBy(function(j){ return j.b; }).value();})
    .flatten()
    .value();

并且它的一个参数化版本:

var array = [{a:1, b:1}, {a:1, b:0}, {a:2, b:2}, {a:1, b:3}];
function multiColumnSort(array, columnNames) {
    var col0 = columnNames[0],
        col1 = columnNames[1];
    return _.chain(array)
        .sortBy(function(i){ return i[col0];})
        .reduce(function(prev, i){
            var ix = prev.length - 1;
            if(!prev[ix] || prev[ix][0][col0] !== i[col0]) {
             prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function(i){ return _.chain(i).sortBy(function(j){ return j[col1]; }).value();})
        .flatten()
        .value();
}
multiColumnSort(array, ['a', 'b']);

而且还有适用于任意列数的参数化版本(从第一次测试看来似乎可行):

var array = [{a:1, b:1, c:9}, {a:1, b:1, c:3}, {a:2, b:2, c:10}, {a:1, b:3, c:0}];
function multiColumnSort(array, columnNames) {
    if(!columnNames || !columnNames.length || array.length === 1) return array;
    var col0 = columnNames[0];
    if(columnNames.length == 1) return _.chain(array).sortBy(function(i){ return i[col0]; }).value();
    
    return _.chain(array)
        .sortBy(function(i){ return i[col0];})
        .reduce(function(prev, i){
            var ix = prev.length - 1;
            if(!prev[ix] || prev[ix][0][col0] !== i[col0]) {
             prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function(i){ return multiColumnSort(i, _.rest(columnNames, 1));})
        .flatten()
        .value();
}
multiColumnSort(array, ['a', 'b', 'c']);

如果您想要能够 反向 排序列:
var array = [{a:1, b:1, c:9}, {a:1, b:1, c:3}, {a:2, b:2, c:10}, {a:1, b:3, c:0}];
function multiColumnSort(array, columnNames) {
    if(!columnNames || !columnNames.length || array.length === 1) return array;
    var col = columnNames[0],
        isString = !!col.toLocaleLowerCase,
        colName = isString ? col : col.name,
        reverse = isString ? false : col.reverse,
        multiplyWith = reverse ? -1 : +1;
    if(columnNames.length == 1) return _.chain(array).sortBy(function(i){ return multiplyWith * i[colName]; }).value();
    
    return _.chain(array)
        .sortBy(function(i){ return multiplyWith * i[colName];})
        .reduce(function(prev, i){
            var ix = prev.length - 1;
            if(!prev[ix] || prev[ix][0][colName] !== i[colName]) {
             prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function(i){ return multiColumnSort(i, _.rest(columnNames, 1));})
        .flatten()
        .value();
}
multiColumnSort(array, ['a', {name:'b', reverse:true}, 'c']);

同时支持函数:

var array = [{a:1, b:1, c:9}, {a:1, b:1, c:3}, {a:2, b:2, c:10}, {a:1, b:3, c:0}];
function multiColumnSort(array, columnNames) {
    if (!columnNames || !columnNames.length || array.length === 1) return array;
    var col = columnNames[0],
        isString = !!col.toLocaleLowerCase,
        isFun = typeof (col) === 'function',
        colName = isString ? col : col.name,
        reverse = isString || isFun ? false : col.reverse,
        multiplyWith = reverse ? -1 : +1,
        sortFunc = isFun ? col : function (i) { return multiplyWith * i[colName]; };

    if (columnNames.length == 1) return _.chain(array).sortBy(sortFunc).value();

    return _.chain(array)
        .sortBy(sortFunc)
        .reduce(function (prev, i) {
            var ix = prev.length - 1;
            if (!prev[ix] || (isFun ? sortFunc(prev[ix][0]) !== sortFunc(i) : prev[ix][0][colName] !== i[colName])) {
                prev.push([]); ix++;
            }
            prev[ix].push(i);
            return prev;
        }, [])
        .map(function (i) { return multiColumnSort(i, _.rest(columnNames, 1)); })
        .flatten()
        .value();
}
multiColumnSort(array, ['a', {name:'b', reverse:true}, function(i){ return -i.c; }]);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接