按属性创建唯一对象数组

64

我创建了一个对象数组,代码如下:

[
    {
        "lat": 12.123,
        "lng": 13.213,
        "city": "New York"
    },
    {
        "lat": 3.123,
        "lng": 2.213,
        "city": "New York"
    },
    {
        "lat": 1.513,
        "lng": 1.113,
        "city": "London"
    }
]

我正在尝试创建一个新数组,过滤掉只包含具有相同city属性的对象(经纬度重复是可以的)。是否有内置的JS或Jquery函数可以实现这一点?


是的,请查看[].forEach()或for循环。 - Kevin B
1
看起来是定制的。你如何确定应该丢弃哪一个?a还是b?编写一个自定义函数来引入这个逻辑。 - Travis J
我会先按城市名称对数组进行排序,然后逐一遍历并删除找到的重复项。如果不先排序,你将需要在数组中的每个条目上进行整个数组的迭代。 - Kevin B
2
我会使用城市名称作为对象属性。 - Ram
一个“集合”可能会对你有所帮助:https://dev59.com/jWsz5IYBdhLWcg3wWmYy - Robb
17个回答

70

在过滤期间,我可能会使用一个 flags 对象 (编辑:不再使用,关于 ES2015 的 Set 请参见答案末尾的注释),像这样:

var flags = {};
var newPlaces = places.filter(function(entry) {
    if (flags[entry.city]) {
        return false;
    }
    flags[entry.city] = true;
    return true;
});

这使用了 ECMAScript5 (ES5) 的 Array#filter,而它是可以被 shimming 的 ES5 增加部分之一(搜索“ es5 shim”以获取几个选项)。

当然,您也可以不使用 filter,只是会变得更冗长:

var flags = {};
var newPlaces = [];
var index;
for (index = 0; index < places.length; ++index) {
    if (!flags[entry.city]) {
        flags[entry.city] = true;
        newPlaces.push(entry);
    }
});

以上两种方法都假设给定城市的第一个对象应该被保留,而其他所有对象则应该被丢弃。


注意:如下面的user2736012所指出的,我的测试if (flags[entry.city])会对那些名字恰好与Object.prototype上存在的属性(如toString)相同的城市返回true。在这种情况下可能性非常小,但有四种方法可以避免这种情况:

  • (我通常喜欢的解决方案)创建没有原型的对象:var flags = Object.create(null);。这是ES5的一个特性。请注意,这不能为像IE8这样的过时浏览器添加shim(除非null是该参数的值)。

  • 使用hasOwnProperty进行测试,例如:if (flags.hasOwnProperty(entry.city))

  • 给你知道不存在于任何Object.prototype属性中的前缀,例如xx

      var key = "xx" + entry.city;
      if (flags[key]) {
          // ...
      }
      flags[key] = true;
    
  • 从 ES2015 开始,您可以使用 Set 替代:

      const flags = new Set();
      const newPlaces = places.filter(entry => {
          if (flags.has(entry.city)) {
              return false;
          }
          flags.add(entry.city);
          return true;
      });
    

1
尽管高度不太可能会有一个名为“toString”或任何其他Object.prototype属性的城市,但使用.hasOwnProperty()仍然不是一个坏主意。 - user2736012
2
是的,这有点过于谨慎了。我承认,特别是对于这种情况。但它确实使其成为更安全的通用解决方案。 - user2736012
1
@undefined:是的,但我认为用户的观点是,如果你概括这个问题,就有可能出现问题。例如,我不会感到惊讶,如果有人推广这种技术,并被constructor属性所困扰。他/她提出了一个非常有用的观点。 - T.J. Crowder
使用Javascript中的新Set类,您可以用新的Set()替换flags对象,并使用flags.has(key)和flags.add(key)。这样,您就不必担心与对象原型属性的冲突了。 - BJ Safdie
最佳答案:几乎所有其他解决方案的时间复杂度都是O(n^2)或类似的。@Robert Byrne的解决方案是次优的,时间复杂度为O(2*n)。而这个解决方案几乎只有O(n)的时间复杂度。 - Conor Mancone

62

以下是 ES6 中最短的解决方案(请参见下面的更新),虽不是最佳实践:

function unique(array, propertyName) {
   return array.filter((e, i) => array.findIndex(a => a[propertyName] === e[propertyName]) === i);
}

性能:https://jsperf.com/compare-unique-array-by-property


1
更新于2018年8月19日: 在Chrome 67.0.3396 / Mac OS X 10.13.6中,获得了比此处建议的其他两个变体更好的性能评分。在使用前,请检查目标浏览器的性能。 - IgorL
3
你是疯狂的天才! - Chaitanya Chauhan
3
@M.A.Naseer抱歉,不知道为什么链接被移除了,无法恢复。 - IgorL
1
这个解决方案的附加好处是它可以与具有某种非平凡“equals”函数的对象一起使用。在这种情况下,“findIndex”调用内部变成了“a => a.equals(e)”。 - scenia

11
您可以使用filter方法和Set数据结构来筛选那些属性值还未被添加到Set中的元素(然后将其添加到Set)。使用逻辑与运算符 (&&) 可以使这一过程仅用一行代码完成。使用这种数据结构的优点是具有亚线性的查找时间(通常为 O(1))。
以下是一个通用函数,用于从对象数组(arr)中根据特定属性(prop)获取唯一数组的对象。请注意,在重复的情况下,只会保留具有该属性值的第一个对象。
const getUniqueBy = (arr, prop) => {
  const set = new Set;
  return arr.filter(o => !set.has(o[prop]) && set.add(o[prop]));
};

演示:

var places = [{
  lat: 12.123,
  lng: 13.213,
  city: 'New York'
}, {
  lat: 3.123,
  lng: 2.213,
  city: 'New York'
}, {
  lat: 3.123,
  lng: 4.123,
  city: 'Some City'
}];
const getUniqueBy = (arr, prop) => {
  const set = new Set;
  return arr.filter(o => !set.has(o[prop]) && set.add(o[prop]));
};
console.log(getUniqueBy(places, 'city'));


被接受的答案太冗长了,而且这个解决方案比Igor的解决方案快3倍。已点赞。 - Nate311

6

https://lodash.com/docs#uniqBy

https://github.com/lodash/lodash/blob/4.13.1/lodash.js#L7711

/**
 * This method is like `_.uniq` except that it accepts `iteratee` which is
 * invoked for each element in `array` to generate the criterion by which
 * uniqueness is computed. The iteratee is invoked with one argument: (value).
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Array
 * @param {Array} array The array to inspect.
 * @param {Array|Function|Object|string} [iteratee=_.identity]
 *  The iteratee invoked per element.
 * @returns {Array} Returns the new duplicate free array.
 * @example
 *
 * _.uniqBy([2.1, 1.2, 2.3], Math.floor);
 * // => [2.1, 1.2]
 *
 * // The `_.property` iteratee shorthand.
 * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
 * // => [{ 'x': 1 }, { 'x': 2 }]
 */

4
我稍微扩展了@IgorL的解决方案,但是扩展了原型并给它一个选择器函数而不是属性,以使其更加灵活:
Array.prototype.unique = function(selector) {
   return this.filter((e, i) => this.findIndex((a) => {
      if (selector) {
        return selector(a) === selector(e);
      }
      return a === e;
    }) === i);
};

用法:

// with no param it uses strict equals (===) against the object
let primArr = ['one','one','two','three','one']
primArr.unique() // ['one','two','three']

let a = {foo:123}
let b = {foo:123}
let fooArr = [a,a,b]
fooArr.unique() //[a,b]

// alternatively, you can pass a selector function
fooArr.unique(item=>item.foo) //[{foo:123}] (first "unique" item returned)

这绝不是最高效的方法,但只要选择器简单且数组不是很大,它就可以正常工作。

在TypeScript中

Array.prototype.unique = function<T>(this: T[], selector?: (item: T) => object): T[] {
   return this.filter((e, i) => this.findIndex((a) => {
      if (selector) {
        return selector(a) === selector(e);
      }
      return a === e;
    }) === i);
};

谢谢你。在我的使用情况下完美地工作! - Matt Inamdar
这是最好的解决方案!如果有人能提供最高效的方法,那就太棒了。但在这些情况下,扩展原型是正确的方法,并且使用选择器也是正确的方法(我正在使用typescript)。 - Worthy7
1
@Worthy7 我也在使用 TypeScript。我在下面添加了类型化版本。 - NSjonas

3
您可以使用Map,这样具有相同键属性(在您的情况下是“city”)的条目只会出现一次。
module.exports = (array, prop) => {
   const keyValueArray = array.map(entry => [entry[prop], entry]);
   const map = new Map(keyValueArray);
   return Array.from(map.values());
};

关于Map和array对象的更多信息请点击此处

Codepen上的基本示例


3

我的建议:

Array.prototype.uniqueCity = function() {
    var processed = [];
    for (var i=this.length-1; i>=0; i--){
        if (processed.indexOf(this[i].city)<0) {
            processed.push(this[i].city);
        } else {
            this.splice(i, 1);
        }
    }
}

使用中:

places.uniqueCity();

或者

Array.prototype.uniqueObjectArray = function(field) {
    var processed = [];
    for (var i=this.length-1; i>=0; i--) {
        if (this[i].hasOwnProperty(field)) {
            if (processed.indexOf(this[i][field])<0) {
                processed.push(this[i][field]);
            } else {
                this.splice(i, 1);
            }
        }
    }
}

places.uniqueObjectArray('city');

通过上述方式,您可以对数组中的任何一个字段进行排序,即使它们并不是每个对象都拥有的
或者:
function uniqueCity(array) {
    var processed = [];
    for (var i=array.length-1; i>=0; i--){
        if (processed.indexOf(array[i].city)<0) {
            processed.push(array[i].city);
        } else {
            array.splice(i, 1);
        }
    }
    return array;
}

places = uniqueCity(places);

7
简单地说,不能简单地玩弄“原型”,只是为了添加某个特定的功能。 - Jakub Kotrs
我必须同意@JakubMichálek的观点。这并不是说它很糟糕,但它似乎对于一个.prototype方法来说有点太具体了。这不是技术问题,更多的是概念上的问题,这是主观的。 - user2736012
2
如果你将它设计得更通用,比如 prototype.unique 并添加一些支持,比如指定键名 function(key) { ... },那么我认为这还可以接受,但是这似乎有点儿小傻。 - Jakub Kotrs
1
当@theblueone非常了解他自己的代码时,在特定项目中扩展原型并不重要。但如果您制作了一些旨在广泛或任何地方使用的开源库,那么您应该三思而后行。 - davidkonrad
另一个问题是它的时间复杂度为O(n^2)。indexOf()也会在完整的处理数组上迭代,并且您正在使用indexOf()的已处理数组将在完成时变得与原始数组长度大致相同。这就是你最终以几乎O(n^2)时间结束的方式(当没有重复项时是最坏情况)。在最好情况下(所有重复项),您将获得O(n)时间。使用查找表的已接受答案将始终接近于O(n)时间。 - Conor Mancone

3

另一个选择:

const uniqueBy = prop => list => {
    const uniques = {}
    return list.reduce(
        (result, item) => {
            if (uniques[item[prop]]) return result
            uniques[item[prop]] = item
            return [...result, item]
        },
        [],
    )
}

const uniqueById = uniqueBy('id')

uniqueById([
    { id: 1, name: 'one' },
    { id: 2, name: 'two' },
    { id: 1, name: 'one' },
    { id: 3, name: 'three' }
])

你可以将其粘贴到控制台中查看它的工作情况。 它应该适用于所呈现的场景以及其他几种情况。

2
const distinctArrayByCity= [
    ...new Map(array.map((item) => [item.city, item])).values(),
];

2
我们可以使用 JavaScript 的 Map 来根据任何属性创建唯一对象列表。
例如:

var places = [{ 'lat': 12.123, 'lng': 13.213, 'city': "New York"},
                { 'lat': 3.123, 'lng': 2.213, 'city': "New York"},
                { 'lat': 43.123, 'lng': 12.213, 'city': "London"}];
                
var cityMap = new Map();
places.forEach(p=> cityMap.set(p.city, p));

console.log([...cityMap.values()]);

执行代码片段以查看结果。

1
有人应该将这个标记为最有帮助的解决方案!!! - Omzig

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