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.
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,Mapkeys don't. - Need insertion-order iteration with guaranteed semantics? ->
Map. Objects mostly preserve order now, butMappromises 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? -> NOTWeakSet. 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)
- Objects inherit from the prototype.
obj.toStringexists even on{}. UseObject.create(null)for a pure dictionary. Critical when keys come from user input (prototype pollution is a real CVE category, not a theoretical concern). - Object keys are always strings (or Symbols).
obj[1]andobj['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 theMappitch in one line. - 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. - 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.freezethem). - 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. - Set has no indices. No
set[0]. Convert with[...set]or iterate withfor...of. JSON.stringify(set)returns'{}'. Sets aren't serializable by default. Use[...set]before stringifying, and rehydrate withnew 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.