Symbols - Unique and Immutable Identifiers
ES6 introduced Symbols, a new primitive data type that creates unique, immutable identifiers. Symbols are primarily used for creating private object properties, defining well-known behaviors, and avoiding property name collisions in objects.
What are Symbols?
A Symbol is a unique and immutable primitive value that can be used as an object property key. Key characteristics:
- Every Symbol is completely unique, even with the same description
- Symbols are immutable (cannot be changed once created)
- Symbols can be used as object property keys
- Symbol properties don't appear in for...in loops or Object.keys()
- Symbols enable meta-programming through well-known symbols
Key Fact: JavaScript now has seven primitive types: string, number, boolean, null, undefined, bigint, and symbol.
Creating Symbols
You create Symbols using the Symbol() function (not a constructor):
// Basic symbol creation
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false - Each Symbol is unique!
// Symbols with descriptions (for debugging)
const id = Symbol('id');
const userId = Symbol('userId');
const debugId = Symbol('debugId');
console.log(id.toString()); // 'Symbol(id)'
console.log(id.description); // 'id' (ES2019+)
// Symbols are always unique, even with same description
const a = Symbol('mySymbol');
const b = Symbol('mySymbol');
console.log(a === b); // false
// Symbol cannot be used with 'new' keyword
// const wrong = new Symbol(); // TypeError!
Important: Unlike strings or numbers, Symbols cannot be implicitly converted to strings. You must use .toString() or .description explicitly.
Using Symbols as Object Keys
Symbols are perfect for creating properties that won't conflict with existing or future properties:
const id = Symbol('id');
const user = {
name: 'Alice',
age: 25,
[id]: 12345 // Symbol as computed property
};
console.log(user[id]); // 12345
console.log(user.name); // 'Alice'
// Symbol properties are hidden from normal enumeration
console.log(Object.keys(user)); // ['name', 'age'] - no Symbol!
console.log(Object.values(user)); // ['Alice', 25] - no Symbol value!
for (let key in user) {
console.log(key); // Only 'name' and 'age', not Symbol
}
// But Symbols are not completely hidden
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
console.log(Reflect.ownKeys(user)); // ['name', 'age', Symbol(id)]
Global Symbol Registry
Sometimes you need to share Symbols across different parts of your application. The global Symbol registry helps with this:
// Symbol.for() - Create or retrieve from global registry
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true - Same Symbol!
// Symbol.keyFor() - Get the key from global registry
console.log(Symbol.keyFor(globalSym1)); // 'app.id'
// Local symbols are not in global registry
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined
// Real-world example: Shared constants
// In file1.js
const STATUS = Symbol.for('app.status');
// In file2.js
const STATUS = Symbol.for('app.status'); // Same Symbol!
Best Practice: Use Symbol.for() when you need to share Symbols across files or modules. Use regular Symbol() for private properties within a single module.
Well-Known Symbols
JavaScript provides built-in Symbols that allow you to customize object behavior. These are called "well-known symbols":
Common Well-Known Symbols:
Symbol.iterator - Define custom iteration behavior
Symbol.toStringTag - Customize Object.prototype.toString()
Symbol.toPrimitive - Control type conversion
Symbol.hasInstance - Customize instanceof behavior
Symbol.species - Specify constructor for derived objects
Symbol.match - Define string matching behavior
Symbol.search - Define string search behavior
Symbol.replace - Define string replace behavior
Symbol.split - Define string split behavior
Symbol.iterator - Custom Iteration
The most commonly used well-known Symbol is Symbol.iterator:
// Create a custom iterable object
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { done: true };
}
}
};
}
};
// Now the object is iterable!
for (let num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// Can use spread operator
console.log([...range]); // [1, 2, 3, 4, 5]
// Can use Array.from()
console.log(Array.from(range)); // [1, 2, 3, 4, 5]
Symbol.toStringTag
Customize the string returned by Object.prototype.toString():
class CustomClass {
constructor(name) {
this.name = name;
}
get [Symbol.toStringTag]() {
return 'CustomClass';
}
}
const obj = new CustomClass('test');
console.log(Object.prototype.toString.call(obj));
// [object CustomClass]
// Compare with default behavior
class RegularClass {
constructor(name) {
this.name = name;
}
}
const regular = new RegularClass('test');
console.log(Object.prototype.toString.call(regular));
// [object Object]
Symbol.toPrimitive
Control how objects are converted to primitive values:
const temperature = {
celsius: 25,
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return this.celsius;
}
if (hint === 'string') {
return `${this.celsius}°C`;
}
// default hint
return this.celsius;
}
};
console.log(+temperature); // 25 (number hint)
console.log(`${temperature}`); // '25°C' (string hint)
console.log(temperature + 5); // 30 (default hint)
// Another example: Custom money object
const price = {
amount: 100,
currency: 'USD',
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return this.amount;
}
return `${this.currency} ${this.amount}`;
}
};
console.log(+price); // 100
console.log(`Price: ${price}`); // 'Price: USD 100'
console.log(price * 2); // 200
Private Properties with Symbols
Before private class fields (#private), Symbols were commonly used for privacy:
const _password = Symbol('password');
const _validatePassword = Symbol('validatePassword');
class User {
constructor(username, password) {
this.username = username;
this[_password] = password;
}
[_validatePassword](input) {
return input === this[_password];
}
login(password) {
if (this[_validatePassword](password)) {
console.log('Login successful!');
return true;
}
console.log('Invalid password');
return false;
}
}
const user = new User('alice', 'secret123');
console.log(user.username); // 'alice'
console.log(user.password); // undefined
console.log(user[_password]); // Only works if you have reference to Symbol
user.login('wrong'); // Invalid password
user.login('secret123'); // Login successful!
// Symbol properties don't show up in console or JSON
console.log(Object.keys(user)); // ['username']
console.log(JSON.stringify(user)); // {"username":"alice"}
Modern Alternative: ES2022 introduced true private fields using # prefix, which provide stronger privacy than Symbols.
Practical Examples
// Example 1: Metadata without name collision
const metadata = Symbol('metadata');
function addMetadata(obj, data) {
obj[metadata] = data;
}
const article = {
title: 'ES6 Features',
author: 'Alice'
};
addMetadata(article, {
created: new Date(),
views: 0
});
console.log(article.title); // 'ES6 Features'
console.log(article[metadata]); // {created: ..., views: 0}
// Example 2: Private methods in object literal
const privateMethod = Symbol('privateMethod');
const calculator = {
[privateMethod](x, y) {
return x + y;
},
add(x, y) {
console.log('Adding numbers...');
return this[privateMethod](x, y);
}
};
console.log(calculator.add(5, 3)); // Adding numbers... 8
// calculator[privateMethod] is hidden from normal access
// Example 3: Preventing property conflicts in libraries
const librarySymbol = Symbol.for('myLibrary.config');
// Library code
function setupLibrary(element) {
element[librarySymbol] = {
initialized: true,
version: '1.0.0'
};
}
// User code won't accidentally override library properties
const div = document.createElement('div');
div.config = 'user config'; // Safe - won't conflict
setupLibrary(div);
Symbol Methods and Properties
const sym = Symbol('mySymbol');
// Properties
console.log(sym.description); // 'mySymbol' (ES2019+)
// Methods
console.log(sym.toString()); // 'Symbol(mySymbol)'
console.log(sym.valueOf()); // Returns the Symbol itself
// Static methods
Symbol.for('key'); // Get/create global Symbol
Symbol.keyFor(sym); // Get key for global Symbol
// Getting Symbol properties
const obj = { [Symbol('a')]: 1, [Symbol('b')]: 2, x: 3 };
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(a), Symbol(b)]
console.log(Reflect.ownKeys(obj)); // ['x', Symbol(a), Symbol(b)]
Practice Exercise:
Challenge: Create a collection class with a private size counter using Symbols.
const _items = Symbol('items');
const _count = Symbol('count');
class Collection {
constructor() {
this[_items] = [];
this[_count] = 0;
}
add(item) {
this[_items].push(item);
this[_count]++;
}
remove(item) {
const index = this[_items].indexOf(item);
if (index > -1) {
this[_items].splice(index, 1);
this[_count]--;
}
}
get size() {
return this[_count];
}
[Symbol.iterator]() {
return this[_items][Symbol.iterator]();
}
}
const collection = new Collection();
collection.add('apple');
collection.add('banana');
console.log(collection.size); // 2
console.log([...collection]); // ['apple', 'banana']
Try it yourself: Add a clear() method and a has(item) method.
Summary
In this lesson, you learned:
- Symbols are unique, immutable primitive values used as property keys
- Every Symbol is unique, even with the same description
- Symbol properties are hidden from normal enumeration (keys, values, for...in)
- Symbol.for() and Symbol.keyFor() manage the global Symbol registry
- Well-known Symbols customize object behavior (iterator, toStringTag, toPrimitive)
- Symbols enable pseudo-private properties before # private fields
- Symbols prevent property name collisions in objects and libraries
Next Up: In the next lesson, we'll explore Iterators and Generators - advanced iteration control!