Back to issues
JAVASCRIPT

Objects and Sets in JavaScript: Beyond the Basics

Objects and Sets are fundamental JavaScript structures. Discover features beyond the obvious like property descriptors, set operations, and when to use each one.

By Thiago Saraiva7 MIN

Every JavaScript dev knows how to create an object with {} and iterate with Object.keys(). But what about property descriptors? Getters and setters? And Sets, do you only use them to dedupe arrays, or do you actually explore their full potential?

Let's go beyond the basics.

Mental model: An Object is a filing cabinet with string-labeled folders: walk up, say the folder name, pull out the contents. A Set is the bouncer's guest list at the door: it refuses duplicates, doesn't care about order of arrival, and answers exactly one question well, "are you on the list?"

Objects: More Than Key-Value

The basics everyone knows:

Property Descriptors: Fine-Grained Control

Every property carries metadata that controls its behavior:

Think of it as turning a sticky note into an engraved plaque. Useful for immutable IDs, hiding internals from iteration, or locking config so nobody (including future-you at 2am) accidentally nukes it.

Getters and Setters: Computed Properties

Looks like a plain property, runs logic underneath. The caller doesn't know, and that's the whole point. Great for validation, derived fields (get fullName()), and lazy computation.

Sets: Collections of Unique Values

Set Operations: Union, Intersection, Difference (2026 Update)

Here's where most posts on the internet are stuck in 2022. Native Set methods shipped across all modern browsers (Chrome 122+, Safari 17+, Firefox 127+, all out since mid-2024). The [...A].filter(...) dance is finally optional:

Faster (native implementation), readable, and the argument can be any iterable with size and has, not just a Set. If you're still shipping the spread-and-filter version, you're writing 2019 JavaScript.

The old trick still works, useful for archaeology in legacy codebases:

Removing Duplicates: The Most Common Use Case

When to Reach for Map or WeakSet Instead

Mini decision tree:

  • Keys aren't strings/symbols? (objects, numbers that must stay numbers) -> Map. Object keys get coerced to strings, Map keys don't.
  • Need insertion-order iteration with guaranteed semantics? -> Map. Objects mostly preserve order now, but Map promises it in the spec.
  • Storing references to DOM nodes or objects that should be garbage-collected when nobody else holds them? -> WeakMap / WeakSet. Classic use: attaching metadata to a DOM element without leaking memory when the element is removed.
  • Just need "is this value present?" with primitives or stable references? -> Set.
  • Need .size, iteration, or to list what's inside? -> NOT WeakSet. WeakSets are deliberately opaque: no iteration, no size. That's the tradeoff for GC-friendliness.

Rule of thumb: if you're using an object as a dictionary and the keys come from user input, stop, you want a Map.

The Pitfalls (Expanded)

  1. Objects inherit from the prototype. obj.toString exists even on {}. Use Object.create(null) for a pure dictionary. Critical when keys come from user input (prototype pollution is a real CVE category, not a theoretical concern).
  2. Object keys are always strings (or Symbols). obj[1] and obj['1'] are the same key. Numbers, booleans, whole objects used as keys, all coerced. {}.toString() returns "[object Object]", so every object-as-key collides. That's the Map pitch in one line.
  3. Set compares objects by reference, not value. new Set([{id:1}, {id:1}]).size === 2. Two different allocations, two different entries, even if they look identical. If you need value-equality, you're either normalizing to a string key or reaching for a library. Records & Tuples (currently Stage 2, no native runtime support yet) would fix this with deep equality by default, but it's not here yet.
  4. Mutating an object already inside a Set breaks invariants silently. The Set still "contains" it by reference, but if the object was deduped based on some field and you change that field, your Set's logical contract is broken. Treat objects inside Sets as frozen in spirit (or actually Object.freeze them).
  5. Adding the same object reference twice is a no-op. set.add(obj); set.add(obj); -> size 1. Adding two different objects with identical shape? Size 2. Sounds obvious, bites everyone once.
  6. Set has no indices. No set[0]. Convert with [...set] or iterate with for...of.
  7. JSON.stringify(set) returns '{}'. Sets aren't serializable by default. Use [...set] before stringifying, and rehydrate with new Set(parsed) on the other side.

FAQ

1. Set.has vs Array.indexOf, does the perf actually matter? Yes, and dramatically. Set.has is O(1) average, Array.indexOf is O(n). For arrays under ~10 items the difference is noise. Over a few thousand, inside a loop, Set wins by orders of magnitude. Benchmark your real case, don't cargo-cult, but the asymptotic story is clear.

2. How do I deep-compare two Sets? Sets compare by reference for objects, so there's no built-in deep equality. For primitive Sets: a.size === b.size && [...a].every(x => b.has(x)). For object Sets, you need a canonical form, serialize each element to a stable string (mind key ordering in JSON), or use a library like lodash's isEqual pairwise. Honest answer: if you need deep Set equality often, you're probably modeling the wrong thing.

3. Object.freeze vs Object.defineProperty, what's the difference? Object.freeze(obj) is the nuclear option: makes every existing property non-writable and non-configurable, and prevents new ones. One call, whole object locked. Object.defineProperty is the surgical tool: lock one property, tweak one descriptor, leave the rest alone. Freeze is shallow by the way, nested objects stay mutable unless you recurse.

4. Can I use Symbols as object keys? Why would I? Yes. obj[Symbol('id')] = 1 works. Symbol keys don't show up in Object.keys, for...in, or JSON.stringify, which makes them perfect for metadata you want attached but not exposed. Libraries use them to stash internal state on user objects without polluting the visible API. Retrieve with Object.getOwnPropertySymbols.

5. Can I use Set for ordered dedup? Yes, and this is underrated. Set preserves insertion order. [...new Set(array)] gives you deduped elements in the order they first appeared. Clean one-liner, no sort needed, no Map-of-booleans manual bookkeeping. It's the right answer nine times out of ten.

Key Takeaways

Objects are the Swiss army knife: descriptors and accessors unlock the advanced tier. Sets aren't just dedup, native set algebra in 2026 makes them a first-class data structure. And when objects-as-dictionaries or "collection of unique things" starts feeling awkward, Map and WeakSet are right there.

Don't limit yourself to the basics. These tools ship with the language. Use them.