Skip to main content
โšก Calmops

Symbol: Unique Identifiers and Well-Known Symbols in JavaScript

Symbol: Unique Identifiers and Well-Known Symbols in JavaScript

Symbols are unique identifiers that enable advanced metaprogramming. This article covers creating symbols, well-known symbols, and practical applications.

Introduction

Symbols enable:

  • Creating unique identifiers
  • Hiding properties
  • Customizing object behavior
  • Implementing protocols
  • Advanced metaprogramming

Understanding Symbols helps you:

  • Build frameworks
  • Create private properties
  • Implement custom protocols
  • Avoid naming conflicts

Creating Symbols

Basic Symbol Creation

// โœ… Good: Create unique symbols
const id = Symbol('id');
const name = Symbol('name');
const email = Symbol('email');

console.log(typeof id); // 'symbol'
console.log(id.toString()); // Symbol(id)

// Each symbol is unique
const id2 = Symbol('id');
console.log(id === id2); // false

Using Symbols as Property Keys

// โœ… Good: Use symbols as object keys
const user = {};
const userId = Symbol('userId');
const userEmail = Symbol('userEmail');

user[userId] = 12345;
user[userEmail] = '[email protected]';

console.log(user[userId]); // 12345
console.log(user[userEmail]); // [email protected]

// Symbols don't appear in Object.keys()
console.log(Object.keys(user)); // []

Symbol Registry

// โœ… Good: Use global symbol registry
const globalId = Symbol.for('userId');
const globalId2 = Symbol.for('userId');

console.log(globalId === globalId2); // true (same symbol)

// Get symbol key
const key = Symbol.keyFor(globalId);
console.log(key); // 'userId'

Well-Known Symbols

Symbol.iterator

// โœ… Good: Implement custom iterator
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next: () => {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { done: true };
      }
    };
  }
}

// Usage
const range = new Range(1, 5);
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

Symbol.asyncIterator

// โœ… Good: Implement async iterator
class AsyncRange {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.asyncIterator]() {
    let current = this.start;
    const end = this.end;

    return {
      async next() {
        await new Promise(resolve => setTimeout(resolve, 100));

        if (current <= end) {
          return { value: current++, done: false };
        }
        return { done: true };
      }
    };
  }
}

// Usage
async function main() {
  const range = new AsyncRange(1, 3);
  for await (const num of range) {
    console.log(num); // 1, 2, 3 (with delays)
  }
}

main();

Symbol.toStringTag

// โœ… Good: Customize object string representation
class User {
  constructor(name) {
    this.name = name;
  }

  get [Symbol.toStringTag]() {
    return 'User';
  }
}

const user = new User('John');
console.log(Object.prototype.toString.call(user)); // [object User]
console.log(user.toString()); // [object User]

Symbol.toPrimitive

// โœ… Good: Customize primitive conversion
class Money {
  constructor(amount) {
    this.amount = amount;
  }

  [Symbol.toPrimitive](/programming/hint) {
    if (hint === 'number') {
      return this.amount;
    }
    if (hint === 'string') {
      return `$${this.amount}`;
    }
    return this.amount;
  }
}

const money = new Money(100);

console.log(+money); // 100 (number hint)
console.log(`${money}`); // $100 (string hint)
console.log(money + 50); // 150 (default hint)

Symbol.hasInstance

// โœ… Good: Customize instanceof behavior
class MyClass {
  static [Symbol.hasInstance](/programming/obj) {
    return obj.customProperty === true;
  }
}

const obj1 = { customProperty: true };
const obj2 = { customProperty: false };

console.log(obj1 instanceof MyClass); // true
console.log(obj2 instanceof MyClass); // false

Symbol.species

// โœ… Good: Control derived object type
class MyArray extends Array {
  static get [Symbol.species]() {
    return Array; // Return Array instead of MyArray
  }
}

const arr = new MyArray(1, 2, 3);
const mapped = arr.map(x => x * 2);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true

Practical Symbol Patterns

Private Properties

// โœ… Good: Create private properties with symbols
const privateData = Symbol('privateData');

class User {
  constructor(name, password) {
    this.name = name;
    this[privateData] = password;
  }

  verifyPassword(password) {
    return this[privateData] === password;
  }
}

const user = new User('John', 'secret123');

console.log(user.name); // 'John'
console.log(user[privateData]); // 'secret123' (accessible with symbol)
console.log(Object.keys(user)); // ['name'] (privateData not enumerable)

Metadata Storage

// โœ… Good: Store metadata with symbols
const metadata = Symbol('metadata');

function addMetadata(obj, key, value) {
  if (!obj[metadata]) {
    Object.defineProperty(obj, metadata, {
      value: new Map(),
      enumerable: false
    });
  }
  obj[metadata].set(key, value);
}

function getMetadata(obj, key) {
  return obj[metadata]?.get(key);
}

// Usage
const user = { name: 'John' };
addMetadata(user, 'role', 'admin');
addMetadata(user, 'permissions', ['read', 'write']);

console.log(getMetadata(user, 'role')); // 'admin'
console.log(Object.keys(user)); // ['name'] (metadata hidden)

Event Emitter with Symbols

// โœ… Good: Event emitter using symbols
const events = Symbol('events');

class EventEmitter {
  constructor() {
    this[events] = new Map();
  }

  on(eventName, handler) {
    if (!this[events].has(eventName)) {
      this[events].set(eventName, []);
    }
    this[events].get(eventName).push(handler);
  }

  emit(eventName, ...args) {
    const handlers = this[events].get(eventName);
    if (handlers) {
      handlers.forEach(handler => handler(...args));
    }
  }

  off(eventName, handler) {
    const handlers = this[events].get(eventName);
    if (handlers) {
      const index = handlers.indexOf(handler);
      if (index > -1) {
        handlers.splice(index, 1);
      }
    }
  }
}

// Usage
const emitter = new EventEmitter();

emitter.on('message', (msg) => {
  console.log('Message:', msg);
});

emitter.emit('message', 'Hello'); // Message: Hello

Mixin Pattern with Symbols

// โœ… Good: Mixin pattern using symbols
const canEat = Symbol('canEat');
const canWalk = Symbol('canWalk');

const eater = {
  [canEat]() {
    return `${this.name} is eating`;
  }
};

const walker = {
  [canWalk]() {
    return `${this.name} is walking`;
  }
};

class Animal {
  constructor(name) {
    this.name = name;
    Object.assign(this, eater, walker);
  }
}

const dog = new Animal('Rex');

console.log(dog[canEat]()); // 'Rex is eating'
console.log(dog[canWalk]()); // 'Rex is walking'
console.log(Object.keys(dog)); // ['name'] (methods hidden)

Advanced Symbol Patterns

Custom Equality

// โœ… Good: Custom equality with symbols
const equals = Symbol('equals');

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  [equals](/programming/other) {
    return this.x === other.x && this.y === other.y;
  }
}

const p1 = new Point(1, 2);
const p2 = new Point(1, 2);
const p3 = new Point(2, 3);

console.log(p1[equals](/programming/p2)); // true
console.log(p1[equals](/programming/p3)); // false

Custom Serialization

// โœ… Good: Custom serialization with symbols
const serialize = Symbol('serialize');
const deserialize = Symbol('deserialize');

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  [serialize]() {
    return JSON.stringify({
      name: this.name,
      email: this.email
    });
  }

  static [deserialize](/programming/json) {
    const data = JSON.parse(json);
    return new User(data.name, data.email);
  }
}

// Usage
const user = new User('John', '[email protected]');
const json = user[serialize]();
const restored = User[deserialize](/programming/json);

console.log(restored.name); // 'John'
console.log(restored.email); // '[email protected]'

Best Practices

  1. Use symbols for private data:

    // โœ… Good
    const privateData = Symbol('privateData');
    obj[privateData] = value;
    
  2. Use Symbol.for() for global symbols:

    // โœ… Good
    const globalId = Symbol.for('userId');
    
  3. Implement well-known symbols:

    // โœ… Good
    [Symbol.iterator]() {
      // Implementation
    }
    
  4. Hide implementation details:

    // โœ… Good
    const internal = Symbol('internal');
    this[internal] = data;
    

Common Mistakes

  1. Using Symbol() for global symbols:

    // โŒ Bad - creates new symbol each time
    const id = Symbol('id');
    const id2 = Symbol('id');
    console.log(id === id2); // false
    
    // โœ… Good - use Symbol.for()
    const id = Symbol.for('id');
    const id2 = Symbol.for('id');
    console.log(id === id2); // true
    
  2. Forgetting symbols are not enumerable:

    // โŒ Bad - expecting symbol in Object.keys()
    const sym = Symbol('key');
    obj[sym] = value;
    console.log(Object.keys(obj)); // [] (symbol not included)
    
    // โœ… Good - use Object.getOwnPropertySymbols()
    console.log(Object.getOwnPropertySymbols(obj)); // [sym]
    
  3. Not implementing well-known symbols:

    // โŒ Bad - custom class not iterable
    class MyClass {
      // No Symbol.iterator
    }
    
    // โœ… Good - implement Symbol.iterator
    class MyClass {
      [Symbol.iterator]() {
        // Implementation
      }
    }
    

Summary

Symbols are powerful for metaprogramming. Key takeaways:

  • Create unique identifiers with Symbol()
  • Use symbols for private properties
  • Implement well-known symbols
  • Hide implementation details
  • Avoid naming conflicts
  • Enable custom protocols
  • Build advanced abstractions

Next Steps

Comments