Exercise
A WeakSet is a collection of objects only - it cannot store primitive values. Unlike a regular Set, a WeakSet holds weak references to its objects, meaning they can be garbage collected if there are no other references to them. WeakSet is not iterable, has no size property, and only supports add(), has(), and delete() methods.
const weakSet = new WeakSet();
let obj1 = { name: "Alice" };
let obj2 = { name: "Bob" };
weakSet.add(obj1);
weakSet.add(obj2);
console.log(weakSet.has(obj1)); // true
console.log(weakSet.has(obj2)); // true
// Primitives are NOT allowed
// weakSet.add(42); // TypeError
// weakSet.add("str"); // TypeError
// WeakSet is NOT iterable
// for (const item of weakSet) {} // TypeError
// When the reference is removed, the object
// becomes eligible for garbage collection
obj1 = null;
// weakSet no longer prevents obj1 from being collected
The symmetric difference of two Sets contains elements that are in either Set but not in both. You can compute it by finding elements unique to each Set and combining them. This is useful for detecting changes between two collections or finding non-overlapping data.
function symmetricDifference(setA, setB) {
const result = new Set();
for (const item of setA) {
if (!setB.has(item)) {
result.add(item);
}
}
for (const item of setB) {
if (!setA.has(item)) {
result.add(item);
}
}
return result;
}
const frontEnd = new Set(["HTML", "CSS", "JavaScript", "React"]);
const backEnd = new Set(["Node.js", "Python", "JavaScript", "SQL"]);
const uniqueSkills = symmetricDifference(frontEnd, backEnd);
console.log([...uniqueSkills]);
// ["HTML", "CSS", "React", "Node.js", "Python", "SQL"]
// "JavaScript" is excluded because it appears in both
A Set uses the SameValueZero algorithm for equality checks, which differs from strict equality (===) in important ways. In a Set, NaN is considered equal to NaN (unlike === where NaN !== NaN). Also, +0 and -0 are treated as the same value. Special values like undefined and null are each stored as distinct entries.
const special = new Set();
// NaN is equal to NaN in a Set
special.add(NaN);
special.add(NaN);
console.log(special.size); // 1 (only one NaN stored)
console.log(special.has(NaN)); // true
// +0 and -0 are treated as the same value
special.add(+0);
special.add(-0);
console.log(special.size); // 2 (NaN and 0)
// undefined and null are distinct values
special.add(undefined);
special.add(null);
console.log(special.size); // 4
console.log([...special]);
// [NaN, 0, undefined, null]
// Compare with strict equality
console.log(NaN === NaN); // false
console.log(+0 === -0); // true
A WeakSet is ideal for tracking whether an object has been processed or visited without preventing the object from being garbage collected. Common use cases include marking DOM nodes as visited, preventing duplicate event handling, or guarding against circular references during recursive operations.
// Track visited nodes during DOM traversal
const visited = new WeakSet();
function processNode(node) {
if (visited.has(node)) {
console.log("Already processed:", node.id);
return;
}
visited.add(node);
console.log("Processing:", node.id);
// perform work on the node...
}
// Guard against circular references
const circularGuard = new WeakSet();
function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (circularGuard.has(obj)) {
return "[Circular Reference]";
}
circularGuard.add(obj);
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
const data = { name: "root", children: [] };
data.children.push(data); // circular reference
console.log(deepClone(data));
// { name: "root", children: ["[Circular Reference]"] }
JavaScript's built-in Set uses reference equality for objects, meaning two objects with identical properties are treated as different entries. To support deep equality, you can create a wrapper class that serializes objects into a canonical string key and uses that key for comparison.
class DeepSet {
constructor() {
this._map = new Map();
}
_serialize(value) {
if (typeof value !== "object" || value === null) {
return JSON.stringify(value);
}
const sorted = Object.keys(value)
.sort()
.reduce((acc, key) => {
acc[key] = value[key];
return acc;
}, {});
return JSON.stringify(sorted);
}
add(value) {
const key = this._serialize(value);
if (!this._map.has(key)) {
this._map.set(key, value);
}
return this;
}
has(value) {
return this._map.has(this._serialize(value));
}
delete(value) {
return this._map.delete(this._serialize(value));
}
get size() {
return this._map.size;
}
values() {
return this._map.values();
}
}
const ds = new DeepSet();
ds.add({ x: 1, y: 2 });
ds.add({ y: 2, x: 1 }); // same content, different key order
console.log(ds.size); // 1 (treated as the same object)
console.log(ds.has({ x: 1, y: 2 })); // true
// Compare with built-in Set
const regular = new Set();
regular.add({ x: 1, y: 2 });
regular.add({ y: 2, x: 1 });
console.log(regular.size); // 2 (different references)
Set.has() performs lookups in O(1) average time because it uses a hash-based structure internally. In contrast, Array.includes() performs a linear scan in O(n) time. This difference becomes significant with large collections where Set can be orders of magnitude faster for membership checks.
// Build a large collection of 1,000,000 items
const size = 1_000_000;
const arr = Array.from({ length: size }, (_, i) => i);
const set = new Set(arr);
const target = size - 1; // worst case for Array
// Benchmark Array.includes()
console.time("Array.includes");
for (let i = 0; i < 1000; i++) {
arr.includes(target);
}
console.timeEnd("Array.includes");
// Array.includes: ~800ms (varies by environment)
// Benchmark Set.has()
console.time("Set.has");
for (let i = 0; i < 1000; i++) {
set.has(target);
}
console.timeEnd("Set.has");
// Set.has: ~0.2ms (varies by environment)
// Practical tip: convert Array to Set when you need
// repeated lookups on the same collection
function findCommon(listA, listB) {
const setB = new Set(listB);
return listA.filter(item => setB.has(item));
}
console.log(findCommon([1, 2, 3, 4], [3, 4, 5, 6]));
// [3, 4]
Starting with ES2025, Set has built-in methods for common set operations. These methods return a new Set and accept any iterable as an argument. They replace the need for manual loops when performing union, intersection, difference, and symmetric difference operations.
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// union() - all elements from both Sets
const united = setA.union(setB);
console.log([...united]);
// [1, 2, 3, 4, 5, 6]
// intersection() - elements common to both Sets
const common = setA.intersection(setB);
console.log([...common]);
// [3, 4]
// difference() - elements in setA but not in setB
const diff = setA.difference(setB);
console.log([...diff]);
// [1, 2]
// symmetricDifference() - elements in either but not both
const symDiff = setA.symmetricDifference(setB);
console.log([...symDiff]);
// [1, 2, 5, 6]
// These methods also accept any iterable
const fromArray = setA.intersection([2, 3, 7]);
console.log([...fromArray]);
// [2, 3]
// Additional utility methods
console.log(setA.isSubsetOf(new Set([1, 2, 3, 4, 5])));
// true
console.log(setA.isSupersetOf(new Set([1, 2])));
// true
console.log(setA.isDisjointFrom(new Set([7, 8, 9])));
// true
Sets are not directly supported by JSON.stringify() - they serialize to an empty object {} by default. To handle this, you can use a replacer function during serialization and a reviver function during parsing to preserve the Set type across JSON round-trips.
// Problem: JSON.stringify ignores Sets
const data = { tags: new Set(["js", "css", "html"]) };
console.log(JSON.stringify(data));
// '{"tags":{}}' - Set lost!
// Solution: Custom replacer and reviver
function replacer(key, value) {
if (value instanceof Set) {
return { __type: "Set", values: [...value] };
}
return value;
}
function reviver(key, value) {
if (value && value.__type === "Set") {
return new Set(value.values);
}
return value;
}
// Serialize
const jsonString = JSON.stringify(data, replacer);
console.log(jsonString);
// '{"tags":{"__type":"Set","values":["js","css","html"]}}'
// Deserialize
const restored = JSON.parse(jsonString, reviver);
console.log(restored.tags instanceof Set); // true
console.log(restored.tags.has("js")); // true
console.log([...restored.tags]);
// ["js", "css", "html"]
// Works with nested structures too
const nested = {
users: new Set(["alice", "bob"]),
config: { roles: new Set(["admin", "editor"]) }
};
const roundTrip = JSON.parse(
JSON.stringify(nested, replacer),
reviver
);
console.log(roundTrip.config.roles.has("admin")); // true
You can use Object.freeze() on a Set to prevent adding or removing elements. However, Object.freeze() is shallow - it freezes the Set object itself but does not prevent mutations on objects stored inside the Set. For a truly immutable Set, you can wrap it in a proxy or create a read-only facade class.
// Approach 1: Object.freeze()
const frozen = Object.freeze(new Set([1, 2, 3]));
try {
frozen.add(4);
} catch (err) {
console.log(err.message);
// "Cannot add property size, object is not extensible"
// or a TypeError depending on the engine
}
console.log(frozen.size); // 3 (unchanged)
// Approach 2: Read-only wrapper class
class ImmutableSet {
#inner;
constructor(iterable) {
this.#inner = new Set(iterable);
}
has(value) {
return this.#inner.has(value);
}
get size() {
return this.#inner.size;
}
values() {
return this.#inner.values();
}
[Symbol.iterator]() {
return this.#inner[Symbol.iterator]();
}
forEach(callback, thisArg) {
this.#inner.forEach(callback, thisArg);
}
}
const safe = new ImmutableSet(["a", "b", "c"]);
console.log(safe.has("a")); // true
console.log(safe.size); // 3
console.log([...safe]); // ["a", "b", "c"]
// No add, delete, or clear methods exposed
// safe.add("d"); // TypeError: safe.add is not a function
// safe.delete("a"); // TypeError: safe.delete is not a function
The power set of a Set S is the set of all possible subsets of S, including the empty set and S itself. For a Set with n elements, the power set contains 2^n subsets. You can build it using a recursive approach or an iterative bit-manipulation approach.
// Recursive approach
function powerSet(inputSet) {
const elements = [...inputSet];
const result = [];
function generate(index, current) {
if (index === elements.length) {
result.push(new Set(current));
return;
}
// Exclude the current element
generate(index + 1, current);
// Include the current element
current.push(elements[index]);
generate(index + 1, current);
current.pop();
}
generate(0, []);
return result;
}
const colors = new Set(["red", "green", "blue"]);
const allSubsets = powerSet(colors);
console.log(allSubsets.length); // 8 (2^3)
allSubsets.forEach(subset => {
console.log([...subset]);
});
// []
// ["blue"]
// ["green"]
// ["green", "blue"]
// ["red"]
// ["red", "blue"]
// ["red", "green"]
// ["red", "green", "blue"]
// Iterative approach using bit manipulation
function powerSetIterative(inputSet) {
const elements = [...inputSet];
const n = elements.length;
const total = 1 << n; // 2^n
const result = [];
for (let mask = 0; mask < total; mask++) {
const subset = new Set();
for (let i = 0; i < n; i++) {
if (mask & (1 << i)) {
subset.add(elements[i]);
}
}
result.push(subset);
}
return result;
}
const nums = new Set([10, 20]);
const subsets = powerSetIterative(nums);
console.log(subsets.length); // 4
subsets.forEach(s => console.log([...s]));
// []
// [10]
// [20]
// [10, 20]