ECMAScript Set

Introduced in ECMAScript 2015 (ES6), the Set object provides a robust way to manage collections of unique values. Unlike a standard Array, which allows duplicate entries and requires $O(n)$ time complexity for membership checks, a Set inherently enforces uniqueness and offers an average time complexity of $O(1)$ for the has operation. This makes it an indispensable tool for developers dealing with deduplication, membership testing, and mathematical set operations.
A Set can be initialized as an empty collection or populated by passing an iterable object (such as an Array, String, or another Set) to its constructor. The constructor automatically iterates through the provided object and discards any duplicate values during initialization.

// Initializing an empty Set
const emptySet = new Set();

// Initializing with an Array (duplicates are automatically removed)
const numbers = new Set([1, 2, 2, 3, 4, 4]);
console.log(numbers); // Set(4) { 1, 2, 3, 4 }

// Initializing with a String
const characters = new Set('hello');
console.log(characters); // Set(4) { 'h', 'e', 'l', 'o' }

Core API and Operations

The Set instance provides a streamlined API for managing its contents. Below are the most frequently used methods and properties:

  • add(value): Adds a new element to the set. Notably, this method returns the Set object itself, enabling method chaining.
  • has(value): Returns a boolean indicating whether a specific value exists within the set.
  • delete(value): Removes the specified element. It returns true if the element was successfully removed and false otherwise.
  • clear(): Removes all elements from the set, resetting it to an empty state.
  • size: A read-only property that returns the number of elements currently in the set. Note: Unlike arrays where you use .length, size is a property and should not be called as a function.
const mySet = new Set();

// Demonstrating method chaining
mySet.add(10).add(20).add(30);

console.log(mySet.has(20)); // true
console.log(mySet.size);    // 3

mySet.delete(20);
console.log(mySet.has(20)); // false

mySet.clear();
console.log(mySet.size);    // 0

The Logic of Uniqueness: SameValueZero

A critical aspect of Set is how it determines if two values are "the same." It employs the SameValueZero algorithm. This algorithm has specific behaviors that differ slightly from the strict equality operator (===):

  1. NaN Equality: Unlike standard JavaScript equality where NaN === NaN is false, the Set treats NaN as equal to NaN. Therefore, a Set can only contain one NaN value.
  2. Signed Zeros: Both +0 and -0 are treated as the same value.
  3. Object Reference: For non-primitive types (objects, arrays, functions), equality is determined by reference, not by content. Two different object literals with the same properties are considered distinct.
const uniquenessSet = new Set();

uniquenessSet.add(NaN);
uniquenessSet.add(NaN);
console.log(uniquenessSet.size); // 1

uniquenessSet.add(+0);
uniquenessSet.add(-0);
console.log(uniquenessSet.size); // 2 (Note: In some environments/specs, +0 and -0 are treated as identical; SameValueZero treats them as equal)

const objA = { id: 1 };
const objB = { id: 1 };
uniquenessSet.add(objA);
uniquenessSet.add(objB);
console.log(uniquenessSet.size); // 4 (objA and objB are different references)

Iterating Over a Set

Set objects maintain the insertion order of their elements. This means when you iterate over a set, the values will appear in the exact order they were added. You can traverse a Set using several methods:

  • for...of loop: The most common and readable way to iterate.
  • forEach(): Similar to the Array.prototype.forEach, though the callback arguments are slightly redundant because Set lacks keys.
  • Iterators: keys(), values(), and entries().
const collection = new Set(['apple', 'banana', 'cherry']);

// Using for...of
for (const item of collection) {
  console.log(item);
}

// Using forEach
collection.forEach((value) => {
  console.log(value);
});

// Iterators
console.log([...collection.keys()]);    // ['apple', 'banana', 'cherry']
console.log([...collection.values()]);  // ['apple', 'banana', 'cherry']
console.log([...collection.entries()]); // [['apple', 'apple'], ['banana', 'banana'], ['cherry', 'cherry']]

Practical Use Case: Array Deduplication

One of the most common patterns in modern JavaScript is using a Set to remove duplicates from an array. By spreading a Set back into an array, you can achieve a clean, one-line deduplication.

const duplicateArray = [1, 2, 2, 3, 4, 4, 5];

// Method 1: Spread operator
const uniqueArray = [...new Set(duplicateArray)];

// Method 2: Array.from()
const uniqueArrayAlt = Array.from(new Set(duplicateArray));

console.log(uniqueArray); // [1, 2, 3, 4, 5]

Mathematical Set Operations

Historically, performing operations like unions or intersections required manual implementation using Array.filter and the spread operator. However, recent ECMAScript updates have introduced native methods to handle these mathematical operations directly.

The Manual Approach (Legacy/Compatibility)

const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);

const union = new Set([...setA, ...setB]);
const intersection = new Set([...setA].filter(x => setB.has(x)));
const difference = new Set([...setA].filter(x => !setB.has(x)));

The Modern Native Approach

Newer environments support built-in methods that are more expressive and efficient:

  • union(other): Returns a new set containing elements from both sets.
  • intersection(other): Returns elements common to both sets.
  • difference(other): Returns elements in the first set that are not in the second.
  • symmetricDifference(other): Returns elements in either set, but not both.
  • isSubsetOf(other), isSupersetOf(other), and isDisjointFrom(other): Boolean checks for set relationships.
const a = new Set([1, 2, 3]);
const b = new Set([3, 4, 5]);

console.log(a.union(b));               // Set(5) { 1, 2, 3, 4, 5 }
console.log(a.intersection(b));        // Set(1) { 3 }
console.log(a.symmetricDifference(b)); // Set(4) { 1, 2, 4, 5 }

Understanding WeakSet

WeakSet is a specialized version of Set designed for specific memory management scenarios. It differs from Set in several key ways:

  1. Object-Only: It can only store objects; primitive values (like strings or numbers) will throw a TypeError.
  2. Weak References: It holds "weak" references to its elements. This means if an object stored in a WeakSet has no other references pointing to it, it can be reclaimed by the Garbage Collector (GC). This prevents memory leaks.
  3. Non-Iterable: Because the GC can remove elements at any time, WeakSet is not iterable. It does not have a size property, a clear() method, or any way to loop through its contents.

WeakSet is ideal for "tagging" objects—for example, marking an object as "processed" or "authenticated" without preventing that object from being deleted from memory when it is no longer needed elsewhere.

const visitedObjects = new WeakSet();
let user = { name: "Alice" };

visitedObjects.add(user);
console.log(visitedObjects.has(user)); // true

user = null; // The object is now eligible for garbage collection

Summary and Best Practices

To use Set effectively in your development workflow, keep these guidelines in mind:

  • Choose Set for Uniqueness: If your primary goal is to ensure no duplicate values exist or to perform high-speed membership checks, Set is superior to Array.
  • Choose Array for Order and Indexing: If you need to access elements by index (arr[0]), sort them, or perform frequent positional operations, stick with Array.
  • Deduplication Pattern: Use [...new Set(array)] as your standard tool for cleaning up duplicate data.
  • Memory Safety: Use WeakSet when you need to associate metadata with objects without interfering with their lifecycle.
  • Mind the Complexity: Remember that while has() is $O(1)$, converting a Set to an Array is an $O(n)$ operation. Avoid unnecessary conversions in performance-critical loops.