如何按多个字段对对象数组进行排序?

374

从这个原始问题中,我该如何对多个字段进行排序?

使用这个稍微调整过的结构,我该如何对城市(升序)和价格(降序)进行排序?

var homes = [
    {"h_id":"3",
     "city":"Dallas",
     "state":"TX",
     "zip":"75201",
     "price":"162500"},
    {"h_id":"4",
     "city":"Bevery Hills",
     "state":"CA",
     "zip":"90210",
     "price":"319250"},
    {"h_id":"6",
     "city":"Dallas",
     "state":"TX",
     "zip":"75000",
     "price":"556699"},
    {"h_id":"5",
     "city":"New York",
     "state":"NY",
     "zip":"00010",
     "price":"962500"}
    ];

我喜欢这个答案提供了一个通用的方法。在我计划使用这段代码的地方,我需要对日期以及其他事物进行排序。如果不考虑有些繁琐,预处理对象的能力似乎很方便。

我尝试将这个答案构建成一个好的通用示例,但是我没有太多的成功。


你想搜索还是排序? - Felix Kling
你使用你链接的第二个答案时遇到了什么问题? - canon
它不够通用。我似乎在添加大量代码,而我只想说sort(["first-field", "ASC"], ["second-field", "DSC"]);。当我尝试添加第一个答案的“primer”逻辑以处理日期、大小写不敏感等时,这变得更加复杂。 - Mike
4
如果你愿意使用lodash,可以查看https://lodash.com/docs/4.17.11#orderBy。 - Deepanshu Arora
按属性排序的模式是 homes.sort((a, b) =>),其中 a.propb.propa.prop - b.prop 数值排序,a.prop.localeCompare(b.prop) 字典序排序,(b.prop < a.prop) - (a.prop < b.prop) 通用排序。要按降序排序而不是升序,请取反返回值(例如,使用 b.prop - a.prop 而不是 a.prop - b.prop)。 - Sebastian Simon
42个回答

418

您可以使用链式排序方法,通过对值的差值进行排序,直到达到一个不等于零的值。

var data = [{ h_id: "3", city: "Dallas", state: "TX", zip: "75201", price: "162500" }, { h_id: "4", city: "Bevery Hills", state: "CA", zip: "90210", price: "319250" }, { h_id: "6", city: "Dallas", state: "TX", zip: "75000", price: "556699" }, { h_id: "5", city: "New York", state: "NY", zip: "00010", price: "962500" }];

data.sort(function (a, b) {
    return a.city.localeCompare(b.city) || b.price - a.price;
});

console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }

或者,使用es6,简单地:

data.sort((a, b) => a.city.localeCompare(b.city) || b.price - a.price);

55
我有所遗漏吗?为什么要用60行代码实现一个可以用1行代码完成的东西。简单、清晰、简洁。我认为这应该是被接受的答案。 - Erez Cohen
36
目前 Stack Overflow 的一个大问题是,老旧的回答往往已经被使用新语言特性(如 ES5-6-7)得到更好的解决方案所取代,但它们仍保留着旧有的得分,因此我们必须向下滚动才能找到“真正”的最佳解决方案!为了解决这个问题,Stack Overflow 应该随着时间的推移逐渐减少投票权重。 - Andy Lorenz
1
@AndyLorenz 完全同意。解决此问题的方法有很多种。例如,用户可以设置,在回答数量超过 z 个且评分高于 y 的回答中,最小化 x 年前的回答。甚至更简单的开始方法是在排序按钮中添加一个"最新"选项。 - OXiGEN
2
@GarrodRan,请将a和b交换。 - Nina Scholz
2
这是一个很好的答案-非常简洁!也许值得解释的是,它之所以有效,是因为当localeCompare()返回零时,两个值匹配,并且为falsey,而-1和+1为truthy。 - Dan King
显示剩余10条评论

329

想要解决你特定问题的非通用、简单解决方案:

homes.sort(
   function(a, b) {          
      if (a.city === b.city) {
         // Price is only important when cities are the same
         return b.price - a.price;
      }
      return a.city > b.city ? 1 : -1;
   });

8
我认为这个演示是原帖作者想要的内容 => http://jsfiddle.net/zJ6UA/533/ - Amin Jafari
4
这个想法是正确的,但逻辑全部错了。你不能从另一个字符串中减去非数字字符串,并且if语句没有意义。 - JLRishe
7
在最后一行可以使用a.localeCompare(b)进行字符串比较... 查看文档 - Michael P
2
第一个城市的比较不应该检查不相等吗?换句话说,这一行不应该是 if (a.city === b.city) 吗?也就是说,如果两个城市相同,则比较价格,否则比较城市。 - Steven Rands
2
非常优雅。如果JavaScript有像LINQ一样的sortBy和后续的thenSortBy就太好了。 - howardlo
显示剩余5条评论

92

一个基于这个回答的多维度排序方法:

更新:这里提供了一个“优化”的版本。它进行了更多的预处理,并为每个排序选项预先创建了一个比较函数。它可能需要更多的内存(因为它为每个排序选项存储了一个函数),但它应该表现得更好,因为它不必在比较期间确定正确的设置。我没有做任何性能分析。

var sort_by;

(function() {
    // utility functions
    var default_cmp = function(a, b) {
            if (a == b) return 0;
            return a < b ? -1 : 1;
        },
        getCmpFunc = function(primer, reverse) {
            var dfc = default_cmp, // closer in scope
                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;
        };

    // actual implementation
    sort_by = function() {
        var fields = [],
            n_fields = arguments.length,
            field, name, reverse, cmp;

        // preprocess sorting options
        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
            });
        }

        // final comparison function
        return function(A, B) {
            var a, b, name, result;
            for (var i = 0; i < n_fields; i++) {
                result = 0;
                field = fields[i];
                name = field.name;

                result = field.cmp(A[name], B[name]);
                if (result !== 0) break;
            }
            return result;
        }
    }
}());

例子用法:

homes.sort(sort_by('city', {name:'price', primer: parseInt, reverse: true}));

演示


原始函数:

var sort_by = function() {
   var fields = [].slice.call(arguments),
       n_fields = fields.length;

   return function(A,B) {
       var a, b, field, key, primer, reverse, result, i;

       for(i = 0; i < n_fields; i++) {
           result = 0;
           field = fields[i];

           key = typeof field === 'string' ? field : field.name;

           a = A[key];
           b = B[key];

           if (typeof field.primer  !== 'undefined'){
               a = field.primer(a);
               b = field.primer(b);
           }

           reverse = (field.reverse) ? -1 : 1;

           if (a<b) result = reverse * -1;
           if (a>b) result = reverse * 1;
           if(result !== 0) break;
       }
       return result;
   }
};

演示


3
记录一下,这个函数仍然可以通过预处理参数列表并创建一个统一的“排序选项数组”来改进。这留给读者作为练习;) - Felix Kling
@Mike:好的...终于完成了;)你看,现在它更复杂了,因为选项被预处理了,但是最终的比较函数(见注释)却更简单了,这(希望)会导致更好的性能。你拥有的排序选项越多,你就越能从这种方法中获得优势。 - Felix Kling

91

以下是一个简单的通用功能方法。使用数组指定排序顺序。在指定降序时,在前面加上负号

var homes = [
    {"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":"162500"},
    {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":"319250"},
    {"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":"556699"},
    {"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":"962500"}
    ];

homes.sort(fieldSorter(['city', '-price']));
// homes.sort(fieldSorter(['zip', '-state', 'price'])); // alternative

function fieldSorter(fields) {
    return function (a, b) {
        return fields
            .map(function (o) {
                var dir = 1;
                if (o[0] === '-') {
                   dir = -1;
                   o=o.substring(1);
                }
                if (a[o] > b[o]) return dir;
                if (a[o] < b[o]) return -(dir);
                return 0;
            })
            .reduce(function firstNonZeroValue (p,n) {
                return p ? p : n;
            }, 0);
    };
}

编辑:在ES6中,它甚至更简短!

"use strict";
const fieldSorter = (fields) => (a, b) => fields.map(o => {
    let dir = 1;
    if (o[0] === '-') { dir = -1; o=o.substring(1); }
    return a[o] > b[o] ? dir : a[o] < b[o] ? -(dir) : 0;
}).reduce((p, n) => p ? p : n, 0);

const homes = [{"h_id":"3", "city":"Dallas", "state":"TX","zip":"75201","price":162500},     {"h_id":"4","city":"Bevery Hills", "state":"CA", "zip":"90210", "price":319250},{"h_id":"6", "city":"Dallas", "state":"TX", "zip":"75000", "price":556699},{"h_id":"5", "city":"New York", "state":"NY", "zip":"00010", "price":962500}];
const sortedHomes = homes.sort(fieldSorter(['state', '-price']));

document.write('<pre>' + JSON.stringify(sortedHomes, null, '\t') + '</pre>')


9
我发现这个函数很不错,所以我进行了一些小的性能改进,取决于解析器,可以提高达90%。我创建了一个gisttest suite - php_nub_qq
根据样本数据,数字似乎按预期排序,但是当我尝试实现时,数字的排序更像字符串... [10,100,11,9]。我错过了什么吗? - Mark Carpenter Jr
@MarkCarpenterJr。不确定您的意思。我的示例可以正确排序数字类型。您能否将您的实现作为问题分享,并在评论中提及我,以便我看到它?然后我可以检查一下。 - chriskelly
@MarkCarpenterJr。刚刚发现了。我已经在评论中添加了解释。 - chriskelly
我创建了一个受此启发的函数,使用localeCompare模式来支持正确的数字排序,我在这里发布了答案:https://stackoverflow.com/a/77205726/3799617 - undefined

35

今天我制作了一个相当通用的多功能排序器。您可以在此处查看thenBy.js: https://github.com/Teun/thenBy.js

它允许您使用标准的Array.sort,但采用firstBy().thenBy().thenBy()样式。它比上面发布的解决方案的代码和复杂性要少得多。


8
当您拨打3次电话时,对于那些第二通电话没有影响的项目,第二通电话不能保证不改变第一通电话的顺序。 - Teun D

25

如何按多个字段对对象数组进行排序:

homes.sort(function(left, right) {
    var city_order = left.city.localeCompare(right.city);
    var price_order = parseInt(left.price) - parseInt(right.price);
    return city_order || -price_order;
});

注释

  • 传递给数组排序的函数应该返回负数/零/正数,以表示小于/等于/大于。
  • a.localeCompare(b)对于字符串是通用支持的,如果a<ba==ba>b,则返回-1、0、1。
  • 减法适用于数字字段,因为a - ba<ba==ba>b时分别返回-、0、+。
  • 最后一行中的||city优先于price
  • 取反以颠倒任何字段的顺序,例如-price_order
  • 将新字段添加到或链中:return city_order || -price_order || date_order;
  • 日期比较使用减法,因为日期计算转换为自1970年以来的毫秒数。
    var date_order = new Date(left.date) - new Date(right.date);注意:Date()返回一个字符串,并且与new Date()构造函数截然不同
  • 布尔比较使用减法,这是保证将true和false转换为1和0(因此减法产生-1或0或1)。
    var goodness_order = Boolean(left.is_good) - Boolean(right.is_good)
    对布尔值进行排序是不寻常的,建议使用Boolean()构造函数来引起注意,即使它们已经是布尔值。

这真的很好。你如何比较布尔值...或将布尔比较转换为-1、0、1? - Sukima
这个答案太棒了。一个完整的排序大师课程。感谢详细的解释。 - equiman

19

虽然这是一个完全的“作弊”,但我认为它对于这个问题很有价值,因为它基本上是一个可以直接使用的封装好的库函数。

如果你的代码可以访问 lodash 或类似 underscore 的兼容库,那么你可以使用 _.sortBy 方法。下面的片段直接从 lodash 文档中复制而来。

尽管示例中注释的结果看起来返回了数组的数组,但这只是显示顺序而不是实际结果,实际结果是对象的数组。

var users = [
  { 'user''fred',   'age'48 },
  { 'user''barney''age'36 },
  { 'user''fred',   'age'40 },
  { 'user''barney''age'34 }
];

_.sortBy(users, [function(o) { return o.user; }]);
 // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]

_.sortBy(users, ['user''age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]

16
下面的函数将允许您对一个包含对象的数组根据一个或多个属性进行排序,可以在每个属性上按升序(默认)或降序进行排序,并允许您选择是否执行区分大小写的比较。默认情况下,此函数执行不区分大小写的排序。 第一个参数必须是包含对象的数组。 随后的参数必须是用逗号分隔的字符串列表,引用要按其排序的不同对象属性。最后一个参数(可选)是一个布尔值,用于选择是否执行区分大小写的排序-对于区分大小写的排序,请使用true。 该函数默认按升序对每个属性/键进行排序。如果要对特定的键进行降序排序,则传入格式为[“property_name”,true]的数组。 以下是一些函数的示例用法,后面是说明(其中homes是包含对象的数组): objSort(homes,“city”) - > 按城市排序(升序,不区分大小写) objSort(homes,['city',true]) - > 按城市排序(降序,不区分大小写) objSort(homes,“city”,true) - > 先按城市排序,然后按价格排序(升序,区分大小写) objSort(homes,“city”,“price”) - > 先按城市排序,然后按价格排序(都是升序,不区分大小写) objSort(homes,“city”,[“price”,true]) - > 先按城市排序(升序),然后按价格排序(降序,不区分大小写) 没有更多的话要说了,这就是函数:
function objSort() {
    var args = arguments,
        array = args[0],
        case_sensitive, keys_length, key, desc, a, b, i;

    if (typeof arguments[arguments.length - 1] === 'boolean') {
        case_sensitive = arguments[arguments.length - 1];
        keys_length = arguments.length - 1;
    } else {
        case_sensitive = false;
        keys_length = arguments.length;
    }

    return array.sort(function (obj1, obj2) {
        for (i = 1; i < keys_length; i++) {
            key = args[i];
            if (typeof key !== 'string') {
                desc = key[1];
                key = key[0];
                a = obj1[args[i][0]];
                b = obj2[args[i][0]];
            } else {
                desc = false;
                a = obj1[args[i]];
                b = obj2[args[i]];
            }

            if (case_sensitive === false && typeof a === 'string') {
                a = a.toLowerCase();
                b = b.toLowerCase();
            }

            if (! desc) {
                if (a < b) return -1;
                if (a > b) return 1;
            } else {
                if (a > b) return -1;
                if (a < b) return 1;
            }
        }
        return 0;
    });
} //end of objSort() function

这里是一些示例数据:

var homes = [{
    "h_id": "3",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "price": 162500
}, {
    "h_id": "4",
    "city": "Bevery Hills",
    "state": "CA",
    "zip": "90210",
    "price": 1000000
}, {
    "h_id": "5",
    "city": "new york",
    "state": "NY",
    "zip": "00010",
    "price": 1000000
}, {
    "h_id": "6",
    "city": "Dallas",
    "state": "TX",
    "zip": "85000",
    "price": 300000
}, {
    "h_id": "7",
    "city": "New York",
    "state": "NY",
    "zip": "00020",
    "price": 345000
}];

11

使用多个键的动态方法:

  • 从排序的每个列/键中筛选唯一值
  • 按顺序或相反顺序排列
  • 根据indexOf(value)键值为每个对象添加权重宽度零填充
  • 使用计算出的权重进行排序

enter image description here

Object.defineProperty(Array.prototype, 'orderBy', {
value: function(sorts) { 
    sorts.map(sort => {            
        sort.uniques = Array.from(
            new Set(this.map(obj => obj[sort.key]))
        );
        
        sort.uniques = sort.uniques.sort((a, b) => {
            if (typeof a == 'string') {
                return sort.inverse ? b.localeCompare(a) : a.localeCompare(b);
            }
            else if (typeof a == 'number') {
                return sort.inverse ? b - a : a - b;
            }
            else if (typeof a == 'boolean') {
                let x = sort.inverse ? (a === b) ? 0 : a? -1 : 1 : (a === b) ? 0 : a? 1 : -1;
                return x;
            }
            return 0;
        });
    });

    const weightOfObject = (obj) => {
        let weight = "";
        sorts.map(sort => {
            let zeropad = `${sort.uniques.length}`.length;
            weight += sort.uniques.indexOf(obj[sort.key]).toString().padStart(zeropad, '0');
        });
        //obj.weight = weight; // if you need to see weights
        return weight;
    }

    this.sort((a, b) => {
        return weightOfObject(a).localeCompare( weightOfObject(b) );
    });
    
    return this;
}
});

使用:

// works with string, number and boolean
let sortered = your_array.orderBy([
    {key: "type", inverse: false}, 
    {key: "title", inverse: false},
    {key: "spot", inverse: false},
    {key: "internal", inverse: true}
]);

输入图像描述


看起来是个有前途的解决方案,但不确定如何使用它?我正在使用TypeScript进行Angular-Ionic项目开发,在组件文件中如何定义它或将其添加到项目中? - Hemang
嗨@Hemang,将代码复制到一个名为array_object_multiple_order.js的文件中,将该文件导入到您的项目中,现在您可以从对象数组中调用.orderBy。 - Leonardo Filipe

8
以下是一种通用的多维排序方法,可以在每个级别上进行反转和/或映射。
此代码为Typescript编写。如需使用Javascript,请查看此JSFiddle

代码:

type itemMap = (n: any) => any;

interface SortConfig<T> {
  key: keyof T;
  reverse?: boolean;
  map?: itemMap;
}

export function byObjectValues<T extends object>(keys: ((keyof T) | SortConfig<T>)[]): (a: T, b: T) => 0 | 1 | -1 {
  return function(a: T, b: T) {
    const firstKey: keyof T | SortConfig<T> = keys[0];
    const isSimple = typeof firstKey === 'string';
    const key: keyof T = isSimple ? (firstKey as keyof T) : (firstKey as SortConfig<T>).key;
    const reverse: boolean = isSimple ? false : !!(firstKey as SortConfig<T>).reverse;
    const map: itemMap | null = isSimple ? null : (firstKey as SortConfig<T>).map || null;

    const valA = map ? map(a[key]) : a[key];
    const valB = map ? map(b[key]) : b[key];
    if (valA === valB) {
      if (keys.length === 1) {
        return 0;
      }
      return byObjectValues<T>(keys.slice(1))(a, b);
    }
    if (reverse) {
      return valA > valB ? -1 : 1;
    }
    return valA > valB ? 1 : -1;
  };
}

用例示例

按姓氏、名字的顺序对人员数组进行排序:

interface Person {
  firstName: string;
  lastName: string;
}

people.sort(byObjectValues<Person>(['lastName','firstName']));

按照语言名称排序代码(请参见map),然后按照版本号降序排列(请参见reverse)。
interface Language {
  code: string;
  version: number;
}

// languageCodeToName(code) is defined elsewhere in code

languageCodes.sort(byObjectValues<Language>([
  {
    key: 'code',
    map(code:string) => languageCodeToName(code),
  },
  {
    key: 'version',
    reverse: true,
  }
]));

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