Create array of unique objects by property
You can filter
using a Set
by only including elements with a property value that has not yet been added to the Set
(after which it should be added to the Set
). This can be accomplished in one line using the logical and operator (&&
). Using this data structure has the advantage of sublinear lookup times (often O(1)
).
Below is a general function to obtain a unique array of objects based on a specific property (prop
) from an array of objects (arr
). Note that in the case of duplicates, only the first object with the property value will be retained.
const getUniqueBy = (arr, prop) => {
const set = new Set;
return arr.filter(o => !set.has(o[prop]) && set.add(o[prop]));
};
Demo:
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'));
Shortest, but not best performance (see update bellow) solution for es6 :
function unique(array, propertyName) {
return array.filter((e, i) => array.findIndex(a => a[propertyName] === e[propertyName]) === i);
}
performance: https://jsperf.com/compare-unique-array-by-property
I'd probably use a flags object during the filtering (edit: I wouldn't anymore, see the note at the end of the answer about ES2015's Set
), like this:
var flags = {};
var newPlaces = places.filter(function(entry) {
if (flags[entry.city]) {
return false;
}
flags[entry.city] = true;
return true;
});
That uses Array#filter
from ECMAScript5 (ES5), which is one of the ES5 additions that can be shimmed (search for "es5 shim" for several options).
You can do it without filter
, of course, it's just a bit more verbose:
var flags = {};
var newPlaces = [];
var index;
for (index = 0; index < places.length; ++index) {
if (!flags[entry.city]) {
flags[entry.city] = true;
newPlaces.push(entry);
}
});
Both of the above assume the first object with a given city should be kept, and all other discarded.
Note: As user2736012 points out below, my test if (flags[entry.city])
will be true for cities with names that happen to be the same as properties that exist on Object.prototype
such as toString
. Very unlikely in this case, but there are four ways to avoid the possibility:
(My usual preferred solution) Create the object without a prototype:
var flags = Object.create(null);
. This is a feature of ES5. Note that this cannot be shimmed for obsolete browsers like IE8 (the single-argument version ofObject.create
can be except when that argument's value isnull
).Use
hasOwnProperty
for the test, e.g.if (flags.hasOwnProperty(entry.city))
Put a prefix on that you know doesn't exist for any
Object.prototype
property, such asxx
:var key = "xx" + entry.city; if (flags[key]) { // ... } flags[key] = true;
As of ES2015, you could use a
Set
instead:const flags = new Set(); const newPlaces = places.filter(entry => { if (flags.has(entry.city)) { return false; } flags.add(entry.city); return true; });