按字符串属性值对对象数组进行排序

4095
我有一个JavaScript对象的数组:
var objs = [ 
    { first_nom: 'Laszlo', last_nom: 'Jamf'     },
    { first_nom: 'Pig',    last_nom: 'Bodine'   },
    { first_nom: 'Pirate', last_nom: 'Prentice' }
];

如何在JavaScript中按last_nom的值对它们进行排序?
我知道sort(a,b),但似乎只适用于字符串和数字。我需要在我的对象中添加toString()方法吗?

2
大小写敏感或不敏感排序? - Peter Mortensen
61个回答

44

最简单的方法:Lodash

(https://lodash.com/docs/4.17.10#orderBy)

此方法类似于_.sortBy,但它允许指定要排序的迭代器的排序顺序。如果未指定orders,则所有值按升序排序。否则,对应值的排序顺序为“desc”表示降序,“asc”表示升序。

参数

collection (Array|Object):要迭代的集合。 [iteratees=[_.identity]] (Array[]|Function[]|Object[]|string[]):要排序的迭代器。 [orders] (string[]):迭代器的排序顺序。

返回值

(Array):返回新的已排序数组。


var _ = require('lodash');
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"}
    ];
    
_.orderBy(homes, ['city', 'state', 'zip'], ['asc', 'desc', 'asc']);

这里不接受负数。 - 1111161171159459134

42

我还没有看到有人提出这种特定的方法,所以我想分享一种简洁的比较方法,适用于字符串数字类型:

const objs = [ 
  { first_nom: 'Lazslo', last_nom: 'Jamf'     },
  { first_nom: 'Pig',    last_nom: 'Bodine'   },
  { first_nom: 'Pirate', last_nom: 'Prentice' }
];

const sortBy = fn => {
  const cmp = (a, b) => -(a < b) || +(a > b);
  return (a, b) => cmp(fn(a), fn(b));
};

const getLastName = o => o.last_nom;
const sortByLastName = sortBy(getLastName);

objs.sort(sortByLastName);
console.log(objs.map(getLastName));

sortBy()的解释

sortBy()接受一个fn函数,该函数从对象中选择一个值用于比较,并返回一个可传递给Array.prototype.sort()的函数。在此示例中,我们正在比较o.last_nom。每当我们收到两个对象时,例如

a = { first_nom: 'Lazslo', last_nom: 'Jamf' }
b = { first_nom: 'Pig', last_nom: 'Bodine' }

我们将它们与 (a, b) => cmp(fn(a), fn(b)) 进行比较。鉴于此:

fn = o => o.last_nom

我们可以将比较函数扩展为(a, b) => cmp(a.last_nom, b.last_nom)。由于JavaScript中逻辑运算符||的工作方式,cmp(a.last_nom, b.last_nom)等同于
if (a.last_nom < b.last_nom) return -1;
if (a.last_nom > b.last_nom) return 1;
return 0;

顺便提一下,在其他语言中,这被称为三路比较“太空船”(<=>)运算符

最后,这是不使用箭头函数的ES5兼容语法:

var objs = [ 
  { first_nom: 'Lazslo', last_nom: 'Jamf'     },
  { first_nom: 'Pig',    last_nom: 'Bodine'   },
  { first_nom: 'Pirate', last_nom: 'Prentice' }
];

function sortBy(fn) {
  function cmp(a, b) { return -(a < b) || +(a > b); }
  return function (a, b) { return cmp(fn(a), fn(b)); };
}

function getLastName(o) { return o.last_nom; }
var sortByLastName = sortBy(getLastName);

objs.sort(sortByLastName);
console.log(objs.map(getLastName));


我喜欢这种方法,但我认为在这里使用“-(fa < fb) || +(fa > fb)”的速记法是一个错误。这是将多个语句压缩成一行代码。相反,使用“if”语句编写的替代方案将更易读,同时仍然相当简洁。我认为为了美观而牺牲可读性是一个错误。 - MSOACC
@MSOACC 感谢您的意见,但我不同意。其他语言实现了一个三路比较运算符,执行相同的比较,因此只需在概念上将其视为fa <=> fb - Patrick Roberts
嘿,帕特里克,我喜欢你的答案,但它只能适用于英文字符 (const cmp = (a, b) => -(a < b) || +(a > b);)。考虑 ["ä", "a", "c", "b"].sort(cmp) => ["a", "b", "c", "ä"],其中 ä 被推到了最后。相反,你应该更新比较函数为:const cmp = (a, b) => a.localeCompare(b); => ["a", "ä", "b", "c"]干杯,感谢你的回答 ;-) - rjanjic
@rjanjic谢谢反馈。我知道它是基于Unicode中字符的代码点进行排序。然而,改用localeCompare会导致无法对数字进行排序,并且速度慢得多。 - Patrick Roberts

36

你可以创建一个具有自定义 toString() 方法的对象类型(该方法由默认比较函数调用),而不是使用自定义比较函数:

function Person(firstName, lastName) {
    this.firtName = firstName;
    this.lastName = lastName;
}

Person.prototype.toString = function() {
    return this.lastName + ', ' + this.firstName;
}

var persons = [ new Person('Lazslo', 'Jamf'), ...]
persons.sort();

35

这里有很多好的答案,但我想指出它们可以很简单地扩展以实现更复杂的排序。你所需要做的就是使用OR运算符来链接比较函数,像这样:

objs.sort((a,b)=> fn1(a,b) || fn2(a,b) || fn3(a,b) )

在这里,fn1fn2等是返回[-1, 0, 1]的排序函数。这将导致“按fn1排序”和“按fn2排序”,这与SQL中的ORDER BY基本相同。
该解决方案基于||运算符的行为,它计算为可以转换为true的第一个计算表达式最简单的形式只有一个内联函数,如下所示:
// ORDER BY last_nom
objs.sort((a,b)=> a.last_nom.localeCompare(b.last_nom) )

如果使用last_nomfirst_nom两个步骤进行排序,结果将如下所示:

// ORDER_BY last_nom, first_nom
objs.sort((a,b)=> a.last_nom.localeCompare(b.last_nom) ||
                  a.first_nom.localeCompare(b.first_nom)  )

一个通用的比较函数 可以像这样:

// ORDER BY <n>
let cmp = (a,b,n)=>a[n].localeCompare(b[n])

这个函数可以扩展支持数字字段、大小写敏感性、任意数据类型等。
您可以通过排序优先级将它们链接在一起来使用它们:
// ORDER_BY last_nom, first_nom
objs.sort((a,b)=> cmp(a,b, "last_nom") || cmp(a,b, "first_nom") )
// ORDER_BY last_nom, first_nom DESC
objs.sort((a,b)=> cmp(a,b, "last_nom") || -cmp(a,b, "first_nom") )
// ORDER_BY last_nom DESC, first_nom DESC
objs.sort((a,b)=> -cmp(a,b, "last_nom") || -cmp(a,b, "first_nom") )

这里的重点是,使用纯函数式JavaScript可以在不使用外部库或复杂代码的情况下帮助你走得更远。它也非常有效,因为不需要进行字符串解析。

32

试一下这个:

直到ES5版本
// Ascending sort
items.sort(function (a, b) {
   return a.value - b.value;
});


// Descending sort
items.sort(function (a, b) {
   return b.value - a.value;
});
ES6及以上版本中
// Ascending sort
items.sort((a, b) => a.value - b.value);

// Descending sort
items.sort((a, b) => b.value - a.value);

2
最佳和简单的解决方案 - Omar Hasan
3
对我不起作用,尝试了其他确实有效的解决方案,但这个不行。试图按字符串排序。 - Thorvald
不能正确传递负数 - 1111161171159459134

30

使用JavaScript的 sort 方法

sort 方法可以通过传入一个比较函数来对数字、字符串甚至对象数组等任何内容进行排序。

比较函数作为可选参数传递给 sort 方法。

该比较函数通常接受两个参数,一般称之为 ab。基于这两个参数,您可以修改排序方法以按照您想要的方式工作。

  1. 如果比较函数返回小于0,则 sort() 方法将在较低索引处对 a 进行排序,即 a 将排在 b 之前。
  2. 如果比较函数返回0,则 sort() 方法将保留元素的位置。
  3. 如果比较函数返回大于0,则 sort() 方法将在较高索引处对 a 进行排序,即 a 将排在 b 之后。

使用上述概念将其应用到你的对象中,其中a将是您的对象属性。

var objs = [
  { first_nom: 'Lazslo', last_nom: 'Jamf' },
  { first_nom: 'Pig', last_nom: 'Bodine' },
  { first_nom: 'Pirate', last_nom: 'Prentice' }
];
function compare(a, b) {
  if (a.last_nom > b.last_nom) return 1;
  if (a.last_nom < b.last_nom) return -1;
  return 0;
}
objs.sort(compare);
console.log(objs)
// for better look use console.table(objs)
output


29

使用示例:

objs.sort(sortBy('last_nom'));

脚本:

/**
 * @description
 * Returns a function which will sort an
 * array of objects by the given key.
 *
 * @param  {String}  key
 * @param  {Boolean} reverse
 * @return {Function}
 */
const sortBy = (key, reverse) => {

  // Move smaller items towards the front
  // or back of the array depending on if
  // we want to sort the array in reverse
  // order or not.
  const moveSmaller = reverse ? 1 : -1;

  // Move larger items towards the front
  // or back of the array depending on if
  // we want to sort the array in reverse
  // order or not.
  const moveLarger = reverse ? -1 : 1;

  /**
   * @param  {*} a
   * @param  {*} b
   * @return {Number}
   */
  return (a, b) => {
    if (a[key] < b[key]) {
      return moveSmaller;
    }
    if (a[key] > b[key]) {
      return moveLarger;
    }
    return 0;
  };
};

谢谢您将其解释清楚,我正在尝试理解为什么数字1、0、-1用于排序。即使有了您上面的解释,看起来非常好 - 我还是没有完全理解它。我总是认为-1是在使用数组长度属性时,即:arr.length = -1表示未找到该项。我可能在混淆一些东西,但您能帮助我理解为什么数字1、0、-1用于确定顺序吗?谢谢。 - Chris22
1
这并不是完全准确的,但可以这样想:传递给 array.sort 的函数会针对数组中的每个项分别调用一次,作为名为“a”的参数。每个函数调用的返回值是如何将项目“a”的索引(当前位置编号)与下一个项目“b”相比进行更改的。索引决定了数组的顺序(0、1、2等)。因此,如果“a”在索引5处,并且您返回-1,则5 + -1 == 4(将其移动到靠前的位置),5 + 0 == 5(保持原位)等。它遍历数组,每次比较两个相邻的元素,直到达到末尾,留下一个排序好的数组。 - Jamie Mason
1
感谢您抽出时间进一步解释。因此,根据您的解释和MDN Array.prototype.sort,我会告诉您我的想法:与ab相比,如果a大于b,则将1添加到a的索引并将其放在b后面;如果a小于b,则从a中减去1并将其放在b前面。如果ab相同,则将0添加到a并将其保留在原地。 - Chris22

27

写简短的代码:

objs.sort((a, b) => a.last_nom > b.last_nom ? 1 : -1)

1
如果值相等怎么办?考虑到有三个值,你可以返回1,-1,0中的哪一个。 - Someone Special
@SomeoneSpecial 那又怎样?结果还是一样的。 - artem
1 || -1 的意思是什么? - Kaleem Elahi
如果我理解正确的话,他正在将其用作位掩码。如果a.last_nom > b.last_nom,则返回1,否则返回-1。根据比较有效地将项目向上或向下移动。 - Robert Talada
1
没有位掩码,表达式a>b && 1|| -1等同于a> b ? 1 : -1,运算符&&返回第一个逻辑“假”值,运算符||返回第一个逻辑“真”值。 - artem
显示剩余2条评论

25

我没有看到任何类似于我这样的实现。这个版本是基于施瓦茨变换惯用语

function sortByAttribute(array, ...attrs) {
  // Generate an array of predicate-objects containing
  // property getter, and descending indicator
  let predicates = attrs.map(pred => {
    let descending = pred.charAt(0) === '-' ? -1 : 1;
    pred = pred.replace(/^-/, '');
    return {
      getter: o => o[pred],
      descend: descending
    };
  });
  // Schwartzian transform idiom implementation. AKA "decorate-sort-undecorate"
  return array.map(item => {
    return {
      src: item,
      compareValues: predicates.map(predicate => predicate.getter(item))
    };
  })
  .sort((o1, o2) => {
    let i = -1, result = 0;
    while (++i < predicates.length) {
      if (o1.compareValues[i] < o2.compareValues[i])
        result = -1;
      if (o1.compareValues[i] > o2.compareValues[i])
        result = 1;
      if (result *= predicates[i].descend)
        break;
    }
    return result;
  })
  .map(item => item.src);
}

以下是如何使用它的示例:

let games = [
  { name: 'Mashraki',          rating: 4.21 },
  { name: 'Hill Climb Racing', rating: 3.88 },
  { name: 'Angry Birds Space', rating: 3.88 },
  { name: 'Badland',           rating: 4.33 }
];

// Sort by one attribute
console.log(sortByAttribute(games, 'name'));
// Sort by mupltiple attributes
console.log(sortByAttribute(games, '-rating', 'name'));

18

排序(更)复杂的对象数组

由于您可能会遇到像这样更复杂的数据结构,我将扩展解决方案。

快速阅读

基于@ege-Özcan非常可爱的答案,提供更多可插拔的版本。

问题

我遇到了下面的情况,但不能改变它。我也不想暂时扁平化对象。出于性能原因和自己实现的乐趣,我也不想使用underscore / lodash。

var People = [
   {Name: {name: "Name", surname: "Surname"}, Middlename: "JJ"},
   {Name: {name: "AAA", surname: "ZZZ"}, Middlename:"Abrams"},
   {Name: {name: "Name", surname: "AAA"}, Middlename: "Wars"}
];

目标

该目标是首先按People.Name.name排序,其次按People.Name.surname排序。

障碍

现在,在基本解决方案中使用括号表示法动态计算要排序的属性。然而,在这种情况下,我们还必须动态构建括号表示法,因为您希望像People['Name.name']这样的东西可以工作,但实际上不行。

另一方面,仅仅使用People['Name']['name']是静态的,并且只允许您进入第n个层级。

解决方案

主要的新增内容将是沿着对象树向下遍历并确定最后一个叶子以及任何中间叶子的值。

var People = [
   {Name: {name: "Name", surname: "Surname"}, Middlename: "JJ"},
   {Name: {name: "AAA", surname: "ZZZ"}, Middlename:"Abrams"},
   {Name: {name: "Name", surname: "AAA"}, Middlename: "Wars"}
];

People.sort(dynamicMultiSort(['Name','name'], ['Name', '-surname']));
// Results in...
// [ { Name: { name: 'AAA', surname: 'ZZZ' }, Middlename: 'Abrams' },
//   { Name: { name: 'Name', surname: 'Surname' }, Middlename: 'JJ' },
//   { Name: { name: 'Name', surname: 'AAA' }, Middlename: 'Wars' } ]

// same logic as above, but strong deviation for dynamic properties 
function dynamicSort(properties) {
  var sortOrder = 1;
  // determine sort order by checking sign of last element of array
  if(properties[properties.length - 1][0] === "-") {
    sortOrder = -1;
    // Chop off sign
    properties[properties.length - 1] = properties[properties.length - 1].substr(1);
  }
  return function (a,b) {
    propertyOfA = recurseObjProp(a, properties)
    propertyOfB = recurseObjProp(b, properties)
    var result = (propertyOfA < propertyOfB) ? -1 : (propertyOfA > propertyOfB) ? 1 : 0;
    return result * sortOrder;
  };
}

/**
 * Takes an object and recurses down the tree to a target leaf and returns it value
 * @param  {Object} root - Object to be traversed.
 * @param  {Array} leafs - Array of downwards traversal. To access the value: {parent:{ child: 'value'}} -> ['parent','child']
 * @param  {Number} index - Must not be set, since it is implicit.
 * @return {String|Number}       The property, which is to be compared by sort.
 */
function recurseObjProp(root, leafs, index) {
  index ? index : index = 0
  var upper = root
  // walk down one level
  lower = upper[leafs[index]]
  // Check if last leaf has been hit by having gone one step too far.
  // If so, return result from last step.
  if (!lower) {
    return upper
  }
  // Else: recurse!
  index++
  // HINT: Bug was here, for not explicitly returning function
  // https://dev59.com/ZWMm5IYBdhLWcg3wi_i0#17528613
  return recurseObjProp(lower, leafs, index)
}

/**
 * Multi-sort your array by a set of properties
 * @param {...Array} Arrays to access values in the form of: {parent:{ child: 'value'}} -> ['parent','child']
 * @return {Number} Number - number for sort algorithm
 */
function dynamicMultiSort() {
  var args = Array.prototype.slice.call(arguments); // slight deviation to base

  return function (a, b) {
    var i = 0, result = 0, numberOfProperties = args.length;
    // REVIEW: slightly verbose; maybe no way around because of `.sort`-'s nature
    // Consider: `.forEach()`
    while(result === 0 && i < numberOfProperties) {
      result = dynamicSort(args[i])(a, b);
      i++;
    }
    return result;
  }
}

示例

可在JSBin上查看示例


3
为什么?这不是对原问题的回答,“the goal”可以通过People.sort((a,b)=>{ return a.Name.name.localeCompare(b.Name.name) || a.Name.surname.localeCompare(b.Name.surname) })来简单解决。 - Tero Tolonen

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