Skip to main content

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:

javascript
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:

javascript
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:

javascript
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

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
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:

javascript
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.

javascript
// 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():

javascript
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:

javascript
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:

javascript
// 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:

javascript
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

  1. 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.

  2. Implement a custom iterable object using Symbol.iterator that produces the Fibonacci sequence up to a given limit.

  3. Use Symbol.toPrimitive to create a Money 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! :)