Skip to main content

WeakMap, WeakSet, and Generators

WeakMap

WeakMap is a collection of key/value pairs where the keys must be objects and the references to the objects are held weakly.

Basic WeakMap Operations

// Creating a WeakMap
const wm = new WeakMap();

// Adding entries
let obj1 = { id: 1 };
let obj2 = { id: 2 };

wm.set(obj1, 'Data for object 1');
wm.set(obj2, 'Data for object 2');

// Retrieving values
console.log(wm.get(obj1)); // 'Data for object 1'

// Checking existence
console.log(wm.has(obj1)); // true

// Deleting entries
wm.delete(obj1);
console.log(wm.has(obj1)); // false

Memory Management with WeakMap

const cache = new WeakMap();

function processObject(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}

const result = expensiveOperation(obj);
cache.set(obj, result);
return result;
}

let user = { id: 1 };
processObject(user);

// When user is set to null and no other references exist
user = null;
// The WeakMap entry is eligible for garbage collection

Private Data Pattern

const privateData = new WeakMap();

class User {
constructor(name, age) {
privateData.set(this, {
name,
age
});
}

getName() {
return privateData.get(this).name;
}

getAge() {
return privateData.get(this).age;
}
}

const user = new User('John', 30);
console.log(user.getName()); // 'John'
// privateData.get(user) is not accessible outside the class

WeakSet

WeakSet is a collection of objects stored in a set where references to the objects are held weakly.

Basic WeakSet Operations

// Creating a WeakSet
const ws = new WeakSet();

// Adding objects
let obj1 = { id: 1 };
let obj2 = { id: 2 };

ws.add(obj1);
ws.add(obj2);

// Checking membership
console.log(ws.has(obj1)); // true

// Removing objects
ws.delete(obj1);
console.log(ws.has(obj1)); // false

Use Case: Object Tagging

const processedItems = new WeakSet();

function processItem(item) {
if (processedItems.has(item)) {
console.log('Already processed');
return;
}

// Process the item
console.log('Processing item:', item);
processedItems.add(item);
}

let item = { data: 'example' };
processItem(item); // Processing item: { data: 'example' }
processItem(item); // Already processed

Generator Functions

Generators are functions that can be paused and resumed, yielding multiple values over time.

Basic Generator Syntax

function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}

const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

Infinite Sequence Generator

function* infiniteSequence() {
let i = 0;
while (true) {
yield i++;
}
}

const numbers = infiniteSequence();
console.log(numbers.next().value); // 0
console.log(numbers.next().value); // 1
// ...continues infinitely

Generator with Input

function* conversation() {
const name = yield "What's your name?";
const age = yield `Hello ${name}, how old are you?`;
return `${name} is ${age} years old`;
}

const conv = conversation();
console.log(conv.next().value); // What's your name?
console.log(conv.next('John').value); // Hello John, how old are you?
console.log(conv.next('30').value); // John is 30 years old

Async Generator Pattern

async function* asyncGenerator() {
let i = 0;
while (i < 3) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i++;
}
}

async function runGenerator() {
for await (const value of asyncGenerator()) {
console.log(value); // Logs 0, 1, 2 with 1-second delays
}
}

Generator Delegation

function* generator1() {
yield 1;
yield 2;
}

function* generator2() {
yield* generator1();
yield 3;
}

const gen = generator2();
for (const value of gen) {
console.log(value); // 1, 2, 3
}

Advanced Generator Patterns

Custom Iteration Protocol

class CustomIterable {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
}

const iterable = new CustomIterable();
for (const value of iterable) {
console.log(value); // 1, 2, 3
}

Error Handling in Generators

function* errorGenerator() {
try {
yield 1;
yield 2;
yield 3;
} catch (error) {
console.log('Error caught:', error);
yield 'Error handled';
}
}

const gen = errorGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error('Test error'))); // Logs error and returns { value: 'Error handled', done: false }

Generator Pipeline

function* filter(numbers, predicate) {
for (const number of numbers) {
if (predicate(number)) {
yield number;
}
}
}

function* map(numbers, transform) {
for (const number of numbers) {
yield transform(number);
}
}

// Usage
const numbers = [1, 2, 3, 4, 5];
const pipeline = map(
filter(numbers, n => n % 2 === 0),
n => n * 2
);

for (const value of pipeline) {
console.log(value); // 4, 8
}

Best Practices

  1. Memory Management
// Good: WeakMap for caching
const cache = new WeakMap();
function cached(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = expensive(obj);
cache.set(obj, result);
return result;
}

// Bad: Using Map for object references
const cache = new Map(); // Memory leak potential
  1. Generator Function Naming
// Good: Clear naming convention
function* generateUserIds() {
let id = 1;
while (true) {
yield `USER_${id++}`;
}
}

// Bad: Unclear purpose
function* gen() {
// ...
}
  1. Error Handling
async function* safeGenerator() {
try {
yield* riskyOperation();
} catch (error) {
yield { error: error.message };
}
}

These features provide powerful tools for:

  • Memory-efficient caching (WeakMap)
  • Object lifetime tracking (WeakSet)
  • Lazy evaluation (Generators)
  • Custom iteration protocols
  • Asynchronous data streams
  • Complex control flow patterns

Remember that WeakMap and WeakSet are specifically designed for memory management scenarios, while Generators excel at handling sequences and asynchronous operations.