6

I have a multiple objects structured like:

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" }
]
let arr = [];

I am using a loop to iterate the data and have a button under each. When the button is clicked, I am pushing the object into an array but I don't want it to be pushed into the array if it already exists. Is there a way to do this without turning my Array into a Set?

hectoraloneo
  • 97
  • 1
  • 6
  • You can use [find()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) to check if an object exists in an array before pushing it. – Nikhil Oct 30 '19 at 01:13
  • As @Nikhil mentioned you can use find or filter to check if that element already exits before pushing Or if you want to do it in one go refer to this link https://stackoverflow.com/questions/38613654/javascript-find-unique-objects-in-array-based-on-multiple-properties – Wamiq Rehman Oct 30 '19 at 01:19
  • checking for object equality is tricky in javascript, the comments for find() or filter() make it sound like it's a simple built in array method that will do the trick, but you either have to compare all of the keys and values manually, or do a thing like JSON.stringify to serialize the objects and compare those strings.... – Dan Oswalt Oct 30 '19 at 01:24
  • do you have to compare all the fields, or just a name? or, could you also attach a unique id to each? – Dan Oswalt Oct 30 '19 at 01:26
  • 3
    @DanOswalt It's not tricky at all, you just have to know what specifically needs to be checked. `JSON.stringify` is insufficient as object properties may be created in any order. There are many libraries that do this in a variety of ways. – Dave Newton Oct 30 '19 at 01:41
  • if saying 'use a library' is the answer, then it's tricky... – Dan Oswalt Oct 30 '19 at 01:55

3 Answers3

5

As the various commenters above have pointed out, the answer to your question is largely dependent upon your requirements. If you need to evaluate only on the basis of a single property-- for instance, name-- it is relatively easy:

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" },
   { name: "a", position: "A", grade: "" },
   { name: "e", position: "E", grade: "" },
   { name: "c", position: "c", grade: "" }
]
let arr = [];

data.forEach(datum => {
  if (!arr.find(item => item.name === datum.name)) {
    arr.push(datum);
  }
});

console.log(arr);

In this scenario, you could even conceivably replace the array with a plain object, if that is an acceptable solution. Then you could simply fetch the result with Object.values. This solution is Set-like without using an actual Set, if there is some requirement that prevents it:

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" },
   { name: "a", position: "A", grade: "" },
   { name: "e", position: "E", grade: "" },
   { name: "c", position: "c", grade: "" }
]
let dataObject = {};

data.forEach(datum => {
  if (!Object.hasOwnProperty(datum.name)) {
    dataObject[datum.name] = datum;
  }
});

console.log(Object.values(dataObject));

If the evaluation needs to happen simply on the basis of the reference pointing to the same object, it is also relatively easy using Array.prototype.includes:

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" },
   { name: "e", position: "E", grade: "" },
];

let data2 = [
  data[1],
  data[3],
  { name: "f", position: "F", grade: "" },
  data[0],
  { name: "g", position: "G", grade: "" },
]

let arr = [];

function pushData(dataArr) {
  dataArr.forEach(datum => {
    if (!arr.includes(datum)) {
      arr.push(datum);
    }
  })

}

pushData(data);
pushData(data2);

console.log(arr);

The only place where this begins to become difficult is if you need to check for equivalent objects-- objects with the same property values but that are actually distinct, different objects. If you have a known and relatively simple object shape, you can write your own check for this. For instance, in your case, if you can expect that you will only ever be comparing objects with a name, position and grade property, you could write a custom comparison pretty easily to handle this:

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" },
   { name: "a", position: "A", grade: "" },
   { name: "e", position: "E", grade: "" },
   { name: "c", position: "c", grade: "" }
]
let arr = [];

function areDatumsEquivalent(datumA, datumB) {
  return (
    datumA.name === datumB.name &&
    datumA.position === datumB.position &&
    datumA.grade === datumB.grade
  );
}

data.forEach(datum => {
  if(!arr.find(arrDatum => areDatumsEquivalent(datum, arrDatum))) {
    arr.push(datum);
  }
});

console.log(arr);

However, if it is not the case that you can expect this sort of uniformity from your objects, then you might be better off bringing in a library to do a deep comparison of your objects (such as lodash isEqual), or looking on Stack Overflow for how people have approached spinning there own solution for deep comparisons:

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" },
   { name: "a", position: "A", grade: "" },
   { name: "e", position: "E", grade: "" },
   { name: "c", position: "c", grade: "" }
]
let arr = [];

data.forEach(datum => {
  if(!arr.find(arrDatum => _.isEqual(datum, arrDatum))) {
    arr.push(datum);
  }
});

console.log(arr);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

Please note that any of the solutions above that leverage nested loops, such as .forEach and .find, will scale poorly as the array sizes increase. As such, using Set or an object/hash based solution might be better from a performance perspective.

Alexander Nied
  • 12,804
  • 4
  • 25
  • 45
  • all the answers here were helpful but this one went into difference scenarios and I appreciate the level of detail. Thank you. – hectoraloneo Oct 30 '19 at 04:36
2

I would recommend to use Set() but anyway, you might have your reasons why you don't want to use it.

Another way how you could achieve your goal would be turing your data into string and using includes (or also indexOf()) to check if the array includes that "string".

Here is an example:

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" } 
];

if (!JSON.stringify(data).includes('{"name":"e","position":"e","grade":""}')) {
 data.push({ name: "e", position: "e", grade: "" });
}

console.log(data);
Reza Saadati
  • 5,018
  • 4
  • 27
  • 64
  • Thank @Reza. I would love to use Set but I am using Vue as a framework and you can't iterate through a Set using their `v-for`, as of the current version of Vue. – hectoraloneo Oct 30 '19 at 04:35
1

I'm including this answer after Alexander's great answer which is much more in depth than mine, but only to include a Set method implementation and a brief explanation as to why you might consider using it.

You don't have to turn your data into a set (at least permanently) if you don't want to. You could utilize Set before the object push (on button click) to verify if the object exists in your array, then push it to the array if not.

let data = [
   { name: "a", position: "A", grade: "" },
   { name: "b", position: "b", grade: "" },
   { name: "c", position: "c", grade: "" },
   { name: "d", position: "d", grade: "" }
];

let arr = [];

function onClick(obj) {
    let tempSet = new Set(arr);
    if (!tempSet.has(obj)) arr.push(obj);
}

Set uses same-value-zero equality to test if objects are equal. That is equal to strict equality === however NaN equals NaN with this algorithm.

There are circumstances where Set won't work for checking equality, as mentioned in the comments to your question, but in this case, you are just trying to push references to the same objects, which makes checking for the object's existence much simpler than checking for nested equality of different objects. For your use case, it should work fine.

Equality in Javascript

Set Documentation

blrzzzt
  • 194
  • 5