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

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个回答

2

我的谦虚建议:

function cmp(a, b) {
  if (a > b) return 1;
  if (a < b) return -1;
  return 0;
}

function objCmp(a, b, fields) {
  for (let field of fields) {
    let ret = 
      cmp(a[field], b[field]);
    if (ret != 0) {
      return ret;
    }
  }
  return 0;
}

通过这两个函数,您可以以非常优雅的方式对对象数组进行排序:

let sortedArray = homes.sort(
  (a, b) => objCmp(
    a, b, ['state', 'city'])
);

1

在这里,您可以尝试更小、更方便的方式按多个字段排序!

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((a, b)=> {
  if (a.city === b.city){
    return a.price < b.price ? -1 : 1
  } else {
    return a.city < b.city ? -1 : 1
  }
})

console.log(homes);


1

通过添加三个相对简单的辅助工具,可以制作出非常直观的功能解决方案。在我们深入探讨之前,让我们从使用方法开始:

function usage(homes, { asc, desc, fallback }) {
  homes.sort(fallback(
    asc(home => home.city),
    desc(home => parseInt(home.price, 10)),
  ));
  console.log(homes);
}

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",
}];

const SortHelpers = (function () {
  const asc  = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
  const desc = (fn) => (a, b) => asc(fn)(b, a);
  const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);
  return { asc, desc, fallback };
})();

usage(homes, SortHelpers);

如果您向下滚动代码片段,您可能已经看到了助手:
const asc  = (fn) => (a, b) => (a = fn(a), b = fn(b), -(a < b) || +(a > b));
const desc = (fn) => (a, b) => asc(fn)(b, a);
const fallback = (...fns) => (a, b) => fns.reduce((diff, fn) => diff || fn(a, b), 0);

让我快速解释一下这些函数的作用。
  • asc creates a comparator function. The provided function fn is called for both the comparator arguments a and b. The results of the two function calls are then compared. -1 is returned if resultA < resultB, 1 is returned if resultA > resultB, or 0 otherwise. These return values correspond with an ascending order direction.

    It could also be written like this:

    function asc(fn) {
      return function (a, b) {
        // apply `fn` to both `a` and `b`
        a = fn(a);
        b = fn(b);
    
        if (a < b) return -1;
        if (a > b) return  1;
        return 0;
        // or `return -(a < b) || +(a > b)` for short
      };
    }
    
  • desc is super simple, since it just calls asc but swaps the a and b arguments, resulting in descending order instead of ascending.

  • fallback (there might be a better name for this) allows us to use multiple comparator functions with a single sort.

    Both asc and desc can be passed to sort by themself.

    homes.sort(asc(home => home.city))
    

    There is however an issue if you want to combine multiple comparator functions. sort only accepts a single comparator function. fallback combines multiple comparator functions into a single comparator.

    The first comparator is called with arguments a and b, if the comparator returns the value 0 (meaning that the values are equal) then we fall back to the next comparator. This continues until a non-0 value is found, or until all comparators are called, in which case the return value is 0.

您也可以向fallback()提供自定义比较函数。例如,如果您想使用localeCompare()而不是使用<>来比较字符串。在这种情况下,您可以将asc(home => home.city)替换为(a, b) => a.city.localeCompare(b.city)

homes.sort(fallback(
  (a, b) => a.city.localeCompare(b.city),
  desc(home => parseInt(home.price, 10)),
));

需要注意的一点是,可以为undefined的值在使用<>进行比较时总是会返回false。因此,如果某个值可能缺失,您可能需要先按其是否存在进行排序。

homes.sort(fallback(
  // homes with optionalProperty first, true (1) > false (0) so we use desc
  desc(home => home.optionalProperty != null), // checks for both null and undefined
  asc(home => home.optionalProperty),
  // ...
))

由于使用 localeCompare() 比较字符串是一件非常普遍的事情,因此您可以将其包含在 asc() 的一部分中。

function hasMethod(item, methodName) {
  return item != null && typeof item[methodName] === "function";
}

function asc(fn) {
  return function (a, b) {
    a = fn(a);
    b = fn(b);

    const areLocaleComparable =
      hasMethod(a, "localeCompare") && hasMethod(b, "localeCompare");

    if (areLocaleComparable) return a.localeCompare(b);

    return -(a < b) || +(a > b);
  };
}

1

您可以使用lodash orderBy函数 lodash

它需要两个参数:字段数组和方向数组('asc','desc')

  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"}
    ];

var sorted =. data._.orderBy(data, ['city', 'price'], ['asc','desc'])

1
// custom sorting by city
const sortArray = ['Dallas', 'New York', 'Beverly Hills'];

const sortData = (sortBy) =>
  data
    .sort((a, b) => {
      const aIndex = sortBy.indexOf(a.city);
      const bIndex = sortBy.indexOf(b.city);

      if (aIndex < bIndex) {
        return -1;
      }

      if (aIndex === bIndex) {
        // price descending
        return b.price- a.price;
      }

      return 1;
    });

sortData(sortArray);

1
易懂易学:
var homes = [
   { 'city': 'Dallas', 'state': 'TX', 'zip': '75201', 'price': '162500'},
   { 'city`enter code here`': 'Bevery Hills', 'state': 'CA', 'zip': '90210', 'price': '319250'},
   { 'city': 'Dallas', 'state': 'TX', 'zip': '75000', 'price': '556699'},
   { 'city': 'New York', 'state': 'NY', 'zip': '00010', 'price': '962500'}
];

homes.sort(compareMultiple(['zip', '-state', 'price']));

function compareMultiple (criteria) {
   return function (a, b) {
      for (let key of criteria) {
         var order = key.includes('-') ? -1 : 1;
         if (!a[key]) return -order;
         if (!b[key]) return order;
         if (!a[key] && ![key]) return 0;
         if (a[key] > b[key]) return order;
         if (a[key] < b[key]) return -order;
      }
      return 0;
   };
}


1
这是@Snowburnt解决方案的通用版本:
var sortarray = [{field:'city', direction:'asc'}, {field:'price', direction:'desc'}];
array.sort(function(a,b){
    for(var i=0; i<sortarray.length; i++){
        retval = a[sortarray[i].field] < b[sortarray[i].field] ? -1 : a[sortarray[i].field] > b[sortarray[i].field] ? 1 : 0;
        if (sortarray[i].direction == "desc") {
            retval = retval * -1;
        }
        if (retval !== 0) {
            return retval;
        }
    }
}


})

这基于我正在使用的排序程序。我没有测试过这个具体的代码,所以可能会有错误,但你可以理解这个想法。这个想法是基于第一个表示差异的字段进行排序,然后停止并转到下一个记录。因此,如果你按三个字段排序,并且比较中的第一个字段足以确定正在排序的两个记录的排序顺序,则返回该排序结果并转到下一个记录。
我在5000条记录上测试了它(实际上使用了更复杂的排序逻辑),它在眨眼之间完成。如果你确实要加载超过1000条记录到客户端,你应该使用服务器端排序和过滤。
这段代码不处理大小写敏感性,但我让读者处理这个微不足道的修改。

1
function sort(data, orderBy) {
        orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
        return data.sort((a, b) => {
            for (let i = 0, size = orderBy.length; i < size; i++) {
                const key = Object.keys(orderBy[i])[0],
                    o = orderBy[i][key],
                    valueA = a[key],
                    valueB = b[key];
                if (!(valueA || valueB)) {
                    console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
                    return [];
                }
                if (+valueA === +valueA) {
                    return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
                } else {
                    if (valueA.localeCompare(valueB) > 0) {
                        return o.toLowerCase() === 'desc' ? -1 : 1;
                    } else if (valueA.localeCompare(valueB) < 0) {
                        return o.toLowerCase() === 'desc' ? 1 : -1;
                    }
                }
            }
        });
    }

Using :

sort(homes, [{city : 'asc'}, {price: 'desc'}])

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"}
    ];
function sort(data, orderBy) {
            orderBy = Array.isArray(orderBy) ? orderBy : [orderBy];
            return data.sort((a, b) => {
                for (let i = 0, size = orderBy.length; i < size; i++) {
                    const key = Object.keys(orderBy[i])[0],
                        o = orderBy[i][key],
                        valueA = a[key],
                        valueB = b[key];
                    if (!(valueA || valueB)) {
                        console.error("the objects from the data passed does not have the key '" + key + "' passed on sort!");
                        return [];
                    }
                    if (+valueA === +valueA) {
                        return o.toLowerCase() === 'desc' ? valueB - valueA : valueA - valueB;
                    } else {
                        if (valueA.localeCompare(valueB) > 0) {
                            return o.toLowerCase() === 'desc' ? -1 : 1;
                        } else if (valueA.localeCompare(valueB) < 0) {
                            return o.toLowerCase() === 'desc' ? 1 : -1;
                        }
                    }
                }
            });
        }
console.log(sort(homes, [{city : 'asc'}, {price: 'desc'}]));


1
最快、最简单的方法是使用OR链接,正如许多人在这里已经建议的那样。对于指定的示例数据,它看起来像这样:
homes.sort((a, b) =>
    a.city.localeCompare(b.city)
    || (Number(b.price) - Number(a.price))
);

但是如果你想要可配置的内容(并且使用 TypeScript),你可以尝试下面的代码:

代码(TypeScript)

export type Comparer<T> = (a: T, b: T) => number;

export type CompareCriterion<TItem, TValue> = {
    selector: (item: TItem) => TValue,
    descending?: boolean,
    comparer?: Comparer<TValue>,
};

export const defaultComparer = <T>(a: T, b: T): number => {
    return a === b ? 0 : a > b ? 1 : -1;
};

export const defaultNumberComparer = (a: number, b: number): number => {
    return a - b;
};

export const StringComparer = (() => {
    const currentLocale = new Intl.Collator(navigator.language, { usage: 'sort', sensitivity: 'variant', caseFirst: 'upper' });
    const currentLocaleIgnoreCase = new Intl.Collator(navigator.language, { usage: 'sort', sensitivity: 'accent', caseFirst: 'upper' });
    const invariantLocale = new Intl.Collator('en', { usage: 'sort', sensitivity: 'variant', caseFirst: 'upper' });
    const invariantLocaleIgnoreCase = new Intl.Collator('en', { usage: 'sort', sensitivity: 'accent', caseFirst: 'upper' });
    return {
        // eslint-disable-next-line @typescript-eslint/unbound-method
        currentLocale: currentLocale.compare,

        // eslint-disable-next-line @typescript-eslint/unbound-method
        currentLocaleIgnoreCase: currentLocaleIgnoreCase.compare,

        // eslint-disable-next-line @typescript-eslint/unbound-method
        invariantLocale: invariantLocale.compare,

        // eslint-disable-next-line @typescript-eslint/unbound-method
        invariantLocaleIgnoreCase: invariantLocaleIgnoreCase.compare,
    };
})();

export const defaultStringComparer = (a: string, b: string): number => {
    return a.localeCompare(b);
};

export const defaultDateComparer = (a: Date, b: Date): number => {
    return a.getTime() - b.getTime();
};

export class ComparerBuilder<TItem> {
    #criteria: ((next?: Comparer<TItem>) => Comparer<TItem>)[] = [];

    add<TValue>(criterion: CompareCriterion<TItem, TValue>): ComparerBuilder<TItem> {
        this.#criteria.push(next => ComparerBuilder.#createComparer(criterion, next));
        return this;
    }

    static #createComparer<TItem, TValue>(
        criterion: CompareCriterion<TItem, TValue>,
        next?: Comparer<TItem>,
    ): Comparer<TItem> {
        const comparer = criterion.comparer ?? defaultComparer;
        return (a: TItem, b: TItem) => {
            const av = criterion.selector(a);
            const bv = criterion.selector(b);
            const comparison = comparer(av, bv);
            if (comparison === 0)
                return next?.(a, b) ?? 0;
            return criterion.descending ? -comparison : comparison;
        };
    }

    build(bottomComparer?: Comparer<TItem>): Comparer<TItem> {
        let comparer = bottomComparer;
        for (let i = this.#criteria.length - 1; i >= 0; i--)
            comparer = this.#criteria[i](comparer);
        return comparer ?? defaultComparer;
    }
}

使用示例

// Declare item type.
type Item = { key: number, code: string, name: string, price: number };

// Build comparer from provided criteria.
const comparer = new ComparerBuilder<Item>()
    .add({ selector: v => v.price })
    .add({ selector: v => v.code, descending: true, comparer: StringComparer.currentLocaleIgnoreCase })
    .add({ selector: v => v.name, comparer: new Intl.Collator('ru').compare })
    .add({ selector: v => v.key, comparer: defaultNumberComparer })
    .build();

// Use built comparer for multiple calls.
const items1: Item[] = [{ key: 1, code: 'FOO', name: 'bar', price: 100.98 }, { key: 2, code: 'FOa', name: 'baz', price: 100.98 }];
// Note: we are using spread operator to prevent original array mutation (sort method works so).
const sortedItems1 = [...items1].sort(comparer);

const items2: Item[] = [{ key: 1, code: 'BAR', name: 'foo', price: 100.98 }];
// Note: we are using spread operator to prevent original array mutation (sort method works so).
const sortedItems2 = [...items2].sort(comparer);

0

对 @chriskelly 答案的改编。


大多数答案忽略了一个问题,如果值在万位以下或百万以上,则价格将无法正确排序。原因是JS按字母顺序排序。这里回答得很好,为什么JavaScript无法对“5,10,1”进行排序和这里如何正确地对整数数组进行排序

最终,我们必须对我们要排序的字段或节点进行一些评估,以确定它是否为数字。我并不是说在这种情况下使用parseInt()是正确的答案,排序结果更重要。

var homes = [{
  "h_id": "2",
  "city": "Dallas",
  "state": "TX",
  "zip": "75201",
  "price": "62500"
}, {
  "h_id": "1",
  "city": "Dallas",
  "state": "TX",
  "zip": "75201",
  "price": "62510"
}, {
  "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(['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 (!parseInt(a[o]) && !parseInt(b[o])) {
          if (a[o] > b[o]) return dir;
          if (a[o] < b[o]) return -(dir);
          return 0;
        } else {
          return dir > 0 ? a[o] - b[o] : b[o] - a[o];
        }
      })
      .reduce(function firstNonZeroValue(p, n) {
        return p ? p : n;
      }, 0);
  };
}
document.getElementById("output").innerHTML = '<pre>' + JSON.stringify(homes, null, '\t') + '</pre>';
<div id="output">

</div>


一个用于测试的小工具


问题出在你尝试排序的数据上。例如中的“price”是字符串格式。如果你想要它在我的示例中正常工作,使用map将你想要转换为数字格式的字段先进行转换。即:const correctedHomes = homes.map(h => ({...h, price: +h.price})) - chriskelly

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