JavaScript Symbols
Introduction
Symbols are a primitive data type introduced in ECMAScript 2015 (ES6). Unlike other primitive types like strings and numbers, Symbols are unique and immutable, meaning once you create a Symbol, its value cannot be changed, and no two Symbols are exactly alike – even if they have the same description.
The primary purpose of Symbols is to create unique property keys that won't collide with other property names, making them ideal for adding properties to objects that you want to keep distinct from any existing or future properties.
Creating Symbols
Basic Symbol Creation
You create a Symbol using the Symbol()
function:
const mySymbol = Symbol();
console.log(mySymbol); // Output: Symbol()
console.log(typeof mySymbol); // Output: "symbol"
Symbols with Descriptions
You can add a description to a Symbol for debugging purposes:
const userIdSymbol = Symbol('userId');
console.log(userIdSymbol); // Output: Symbol(userId)
This description doesn't affect the Symbol's uniqueness - it's merely for identification when you log or debug your code.
Uniqueness of Symbols
Every Symbol is unique, even if they have the same description:
const sym1 = Symbol('test');
const sym2 = Symbol('test');
console.log(sym1 === sym2); // Output: false
console.log(sym1 == sym2); // Output: false
Using Symbols as Object Properties
One of the most common uses for Symbols is to create unique property keys in objects.
Adding Symbol Properties to Objects
const user = {
name: "Alice",
age: 25
};
const idSymbol = Symbol('id');
user[idSymbol] = "12345";
console.log(user.name); // Output: "Alice"
console.log(user[idSymbol]); // Output: "12345"
Symbols in Object Literals
You can also define Symbol properties directly in object literals using computed property syntax:
const nameSymbol = Symbol('name');
const ageSymbol = Symbol('age');
const person = {
[nameSymbol]: "Bob",
[ageSymbol]: 30,
job: "Developer"
};
console.log(person.job); // Output: "Developer"
console.log(person[nameSymbol]); // Output: "Bob"
Symbol Properties Are Not Enumerable
An important feature of Symbols is that they don't show up in standard property enumeration methods:
const id = Symbol('id');
const user = {
name: "Charlie",
[id]: 7890
};
console.log(Object.keys(user)); // Output: ["name"]
console.log(Object.getOwnPropertyNames(user)); // Output: ["name"]
for (let key in user) {
console.log(key); // Only outputs: "name"
}
To access Symbol properties, you need specific methods:
// Get all Symbol properties
const symbols = Object.getOwnPropertySymbols(user);
console.log(symbols); // Output: [Symbol(id)]
// Access the Symbol property
console.log(user[symbols[0]]); // Output: 7890
Well-Known Symbols
JavaScript has built-in Symbols called "well-known Symbols" that allow you to customize behavior of various JavaScript operations. These Symbols are properties of the Symbol
constructor.
Symbol.iterator
One of the most commonly used well-known Symbols is Symbol.iterator
, which allows you to define how an object should behave in a for...of
loop:
const customIterable = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
for (let item of customIterable) {
console.log(item); // Outputs: 1, 2, 3, 4, 5
}
Symbol.toPrimitive
Another useful well-known Symbol is Symbol.toPrimitive
, which lets you control how objects are converted to primitive values:
const myObject = {
value: 42,
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return this.value;
case 'string':
return `The value is ${this.value}`;
default: // 'default'
return `${this.value}`;
}
}
};
console.log(+myObject); // Output: 42 (number conversion)
console.log(String(myObject)); // Output: "The value is 42" (string conversion)
console.log(myObject + ""); // Output: "42" (default conversion)
Symbol Registry with Symbol.for() and Symbol.keyFor()
Sometimes you want to create Symbols that are accessible globally. The Symbol.for()
method lets you create or access Symbols in a global Symbol registry.
// Create a Symbol in the global registry
const globalSymbol = Symbol.for('globalId');
// Retrieve the same Symbol from elsewhere in your code
const sameGlobalSymbol = Symbol.for('globalId');
console.log(globalSymbol === sameGlobalSymbol); // Output: true
You can retrieve the key used to register a global Symbol using Symbol.keyFor()
:
const sym = Symbol.for('app.userId');
console.log(Symbol.keyFor(sym)); // Output: "app.userId"
// Regular symbols don't have a key in the registry
const regularSym = Symbol('description');
console.log(Symbol.keyFor(regularSym)); // Output: undefined
Practical Use Cases for Symbols
1. Private Properties
Symbols can be used to create "semi-private" properties that won't clash with other properties:
function createUser(name, age) {
const userSymbols = {
id: Symbol('userId'),
token: Symbol('secretToken')
};
return {
name,
age,
[userSymbols.id]: generateUniqueId(), // Some function that creates unique IDs
[userSymbols.token]: generateToken(), // Some function that creates tokens
// Public method to verify token
verifyToken(providedToken) {
return this[userSymbols.token] === providedToken;
}
};
}
2. Extending Built-in Objects Safely
Symbols allow you to extend built-in objects without fear of future property collisions:
// Add a method to Array.prototype safely
Array.prototype[Symbol('shuffle')] = function() {
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
return this;
};
// This won't conflict with any existing or future Array methods
3. Implementing Special Object Behaviors
We can use well-known Symbols to implement special behaviors for custom objects:
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return this.celsius;
case 'string':
return `${this.celsius}°C`;
default:
return this.celsius;
}
}
}
const temp = new Temperature(25);
console.log(+temp); // Output: 25
console.log(`The temperature is ${temp}`); // Output: "The temperature is 25°C"
console.log(temp + 5); // Output: 30
Summary
JavaScript Symbols are a unique primitive type that provide guaranteed unique identifiers. They're especially useful for:
- Creating non-enumerable object properties
- Defining special object behaviors using well-known Symbols
- Preventing property name collisions
- Implementing "private" or hidden properties
- Extending objects safely
Symbols may seem abstract at first, but they solve several important problems in JavaScript and are a key part of modern JavaScript development, especially when working with libraries and frameworks.
Further Learning
Exercises
-
Create a
Person
class with a Symbol property for a secret PIN code. Add methods to verify the PIN but make sure the PIN itself cannot be enumerated. -
Implement a custom iterable object using
Symbol.iterator
that produces the Fibonacci sequence up to a given limit. -
Use
Symbol.toPrimitive
to create aMoney
class that behaves differently when used in mathematical operations versus string contexts.
Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)