Skip to content

Instantly share code, notes, and snippets.

@phillippelevidad
Created November 3, 2023 10:03
Show Gist options
  • Save phillippelevidad/d44aef576dd9f97e046987302992dbf8 to your computer and use it in GitHub Desktop.
Save phillippelevidad/d44aef576dd9f97e046987302992dbf8 to your computer and use it in GitHub Desktop.
/**
* The UniquePropertySet class is designed to manage a collection of objects,
* ensuring that each object is unique based on its properties and values
* rather than its reference in memory.
*
* Beware, though, that this implementation is not optimized for performance,
* and that the JSON.stringify method does not handle non-serializable values
* consistently, such as functions, undefined, or circular references, which
* will cause objects containing such values to not be handled correctly.
*/
class UniquePropertySet {
constructor() {
this.map = new Map();
}
add(obj) {
const key = this._serialize(obj);
if (!this.map.has(key)) {
this.map.set(key, obj);
}
}
has(obj) {
const key = this._serialize(obj);
return this.map.has(key);
}
delete(obj) {
const key = this._serialize(obj);
return this.map.delete(key);
}
_serialize(obj) {
// Sort the object's keys to ensure consistent order
return JSON.stringify(obj, Object.keys(obj).sort());
}
clear() {
return this.map.clear();
}
get size() {
return this.map.size;
}
values() {
return this.map.values();
}
*[Symbol.iterator]() {
for (let value of this.map.values()) yield value;
}
}
// Usage
const assert = require("assert");
const uniqueObjects = new UniquePropertySet();
uniqueObjects.add({ a: 1, b: 2 });
uniqueObjects.add({ b: 2, a: 1 }); // Won't be added, as it's considered a duplicate
assert.strictEqual(uniqueObjects.size, 1);
uniqueObjects.add({ a: 1, b: 2, c: 3 }); // Will be added, as it's a different object
assert.strictEqual(uniqueObjects.size, 2);
uniqueObjects.delete({ a: 1, b: 2 }); // Correctly deletes the first entry
assert.strictEqual(uniqueObjects.size, 1);
// Nested objects are supported too
uniqueObjects.clear();
uniqueObjects.add({ a: 1, b: { c: 2 } });
uniqueObjects.add({ a: 1, b: { c: 2 } });
assert.strictEqual(uniqueObjects.size, 1);
// Even nested objects are supported
uniqueObjects.clear();
uniqueObjects.add({ a: 1, b: { d: 3, c: 2 } });
uniqueObjects.add({ a: 1, b: { c: 2, d: 3 } });
assert.strictEqual(uniqueObjects.size, 1);
@ganes369
Copy link

Oi, vi seu comentário lá da plataforma do erick wendel training do treinamento de js expert e andei pensando em situações em que eu tenha que verificar a existência de objetos repetidos, ñ seria + simples por exemplo fazer digamos 1 for..of e comparar prop por prop ex: obj.name === newObj.name e assim por diante?

@phillippelevidad
Copy link
Author

Fala aí @ganes369, tudo bom? Não sei de qual comentário específico você está falando; pode me passar o nome da aula pra eu conferir?

De qualquer forma, aqui está uma função que eu fiz recentemente como utilitária pra um projeto pequeno em que não queria instalar biblioteca. Ela faz o que você falou: usa um for..of pra iterar pelas chaves dos objetos e comparar igualdade.

function deepEqual(obj1, obj2) {
    if (obj1 === obj2) {
        return true;
    }

    if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 == null || obj2 == null) {
        return false;
    }

    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);

    if (keys1.length !== keys2.length) {
        return false;
    }

    for (const key of keys1) {
        if (!keys2.includes(key)) {
            return false;
        }

        if (!deepEqual(obj1[key], obj2[key])) {
            return false;
        }
    }

    return true;
}

// Example usage
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(deepEqual(obj1, obj2)); // true

Mas ela pode falhar para propriedades que contenham um Date ou RegExp. Para casos mais robustos, o ideal é usar uma implementação pronta tipo a _.isEqual do lodash.

@ganes369
Copy link

ganes369 commented Dec 1, 2023

Opa, blz e ai? foi da aula de Set e WeakSet no módulo de advanced js data types! obg por ter respondido a minha dúvida... deveria ter replicado por lá o comentário, porém a plataforma n permite...

@ganes369
Copy link

ganes369 commented Dec 1, 2023

você deixou esse comentário lá (https://gist.github.com/phillippelevidad/d44aef576dd9f97e046987302992dbf8 ), e tinha falado sobre o problema de performance, dai so comentei para tirar a minha duvida se com for..of o algoritmo ficaria +performático e que sou meio iniciante e ñ entendo mt sobre essas qst (desconsidere se falei alguma besteira)

@phillippelevidad
Copy link
Author

Vi meu comentário na aula de Set e WeakSet. Lá eu falei que essa solução que eu botei aqui no Gist tem um problema inerente de performance porque usa JSON.stringify. Daí, se eu entendi bem a sua dúvida aqui, é porque você está curioso se fazer um for..of resolveria, ou seja, se seria mais rápido.

Em tese, sim :) mas só tem um jeito da gente saber, e é fazendo um benchmark. O Erick ensina isso mais à frente no curso, mas bora rodar um benchmark aqui rapidão e ver o que acontece.


Rodei o código abaixo 3 vezes, pra gente ter uma análise melhor de 3, e olha só os tempos:

deepEqual x 941,275 ops/sec ±2.13% (85 runs sampled)
JSON.stringify x 94,887 ops/sec ±1.91% (88 runs sampled)
Fastest is deepEqual

deepEqual x 921,202 ops/sec ±2.98% (82 runs sampled)
JSON.stringify x 91,463 ops/sec ±1.53% (84 runs sampled)
Fastest is deepEqual

deepEqual x 944,692 ops/sec ±1.74% (85 runs sampled)
JSON.stringify x 88,985 ops/sec ±2.92% (77 runs sampled)
Fastest is deepEqual

Você pode ver que, no último teste, o deepEqual rodou 944.692 por segundo, contra apenas 88.985 do JSON.stringify. E os outros dois testes não ficam muito diferentes, então podemos que, na média, o deepEqual que mandei pra você no comentário acima é 10x mais rápido que o JSON.stringify!

Segue o código do teste, usando a lib https://www.npmjs.com/package/benchmark. Eu crio dois objetos idênticos e deixo rodar a comparação nas duas formas que estamos testando.

const deepEqual = require("./deepEqual.js");
const Benchmark = require("benchmark");

const obj1 = {
  firstName: "John",
  lastName: "Doe",
  dateOfBirth: new Date("1980-01-01"),
  address: {
    street: "North Pole",
    city: "Arctic Circle",
    country: "Earth",
  },
  orders: [
    {
      product: {
        name: "iPhone",
        price: 699,
      },
      quantity: 1,
    },
    {
      product: {
        name: "MacBook Pro",
        price: 1299,
      },
      quantity: 2,
    },
  ],
};

const obj2 = {
  firstName: "John",
  lastName: "Doe",
  dateOfBirth: new Date("1980-01-01"),
  address: {
    street: "North Pole",
    city: "Arctic Circle",
    country: "Earth",
  },
  orders: [
    {
      product: {
        name: "iPhone",
        price: 699,
      },
      quantity: 1,
    },
    {
      product: {
        name: "MacBook Pro",
        price: 1299,
      },
      quantity: 2,
    },
  ],
};

const suite = new Benchmark.Suite();

suite
  // add tests
  .add("deepEqual", function () {
    return deepEqual(obj1, obj2);
  })
  .add("JSON.stringify", function () {
    return JSON.stringify(obj1) === JSON.stringify(obj2);
  })
  // add listeners
  .on("cycle", function (event) {
    console.log(String(event.target));
  })
  .on("complete", function () {
    console.log("Fastest is " + this.filter("fastest").map("name"));
  })
  // run async
  .run({ async: true });

Massa, né :)

@ganes369
Copy link

ganes369 commented Dec 2, 2023

Opa, iae blz? era isso msm "Daí, se eu entendi bem a sua dúvida aqui, é porque você está curioso se fazer um for..of resolveria, ou seja, se seria mais rápido" eu ainda ñ chegai nesse módulo ainda "benchmark" msm valeuzão pela resposta...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment