Symbols and Iterators in JavaScript
JavaScript’s Symbol primitive and iterator protocol are powerful features introduced in ES6 that often fly under the radar. Symbols provide a way to create unique property keys, while iterators enable custom iteration behavior. Together, they form the foundation for many modern JavaScript patterns and built-in language features.
Understanding Symbols
A Symbol is a primitive data type introduced in ES6. Unlike strings, numbers, or booleans, each Symbol is guaranteed to be unique. This makes them perfect for creating object properties that won’t collide with other keys.
Creating Symbols
You create a Symbol using the Symbol() function. Optionally, you can provide a description for debugging purposes:
const sym1 = Symbol();
const sym2 = Symbol("mySymbol");
const sym3 = Symbol("mySymbol");
console.log(sym1); // Symbol()
console.log(sym2 === sym3); // false - each Symbol is unique
Even when two Symbols have the same description, they remain distinct. The description is purely for readability and debugging.
Using Symbols as Property Keys
Symbols work beautifully as object property keys. They create properties that won’t conflict with string keys:
const uniqueKey = Symbol("unique");
const user = {
name: "Alice",
[uniqueKey]: "secret-value"
};
console.log(user.name); // "Alice"
console.log(user[uniqueKey]); // "secret-value"
// Symbol keys don't appear in Object.keys()
console.log(Object.keys(user)); // ["name"]
// But they do appear in Object.getOwnPropertySymbols()
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(unique)]
This makes Symbols ideal for adding private-like properties to objects, though they’re not truly private since they’re discoverable via Object.getOwnPropertySymbols().
Well-Known Symbols
JavaScript provides several well-known symbols that override default language behavior. These are global Symbol properties that modify how objects behave in specific scenarios.
Symbol.iterator
The most important well-known symbol is Symbol.iterator. It defines the default iterator for an object, enabling for...of loops and the spread operator:
const numbers = [1, 2, 3];
// Arrays have Symbol.iterator by default
const iterator = numbers[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Symbol.toStringTag
This symbol customizes the output of Object.prototype.toString():
const user = {
[Symbol.toStringTag]: "User"
};
console.log(user.toString()); // "[object User]"
Symbol.hasInstance
This symbol lets you customize the behavior of the instanceof operator:
class EvenNumbers {
static [Symbol.hasInstance](number) {
return number % 2 === 0;
}
}
console.log(4 instanceof EvenNumbers); // true
console.log(7 instanceof EvenNumbers); // false
The Iterator Protocol
The iterator protocol defines how objects produce a sequence of values. An object is an iterator when it implements a next() method that returns an object with value and done properties.
Creating Custom Iterators
You can create your own iterator by implementing the required interface:
function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
const iterator = createRangeIterator(1, 3);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
This iterator produces values from 1 to 3, then signals completion. The done property tells the consumer when to stop iterating.
The Iterable Protocol
The iterable protocol allows objects to define their iteration behavior. An object is iterable when it implements a method keyed by Symbol.iterator that returns an iterator.
Making Objects Iterable
To make your custom objects iterable, add a [Symbol.iterator]() method:
const fibonacci = {
[Symbol.iterator]() {
let a = 0, b = 1;
return {
next() {
const result = { value: b, done: false };
[a, b] = [b, a + b];
return result;
}
};
}
};
for (const num of fibonacci) {
if (num > 100) break;
console.log(num); // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
}
This example creates an infinite Fibonacci sequence that stops when values exceed 100. The for...of loop handles the iterator protocol automatically.
Practical Iterable: A Range Object
Here’s a more practical example—a range object that iterates over numbers within a range:
const Range = (start, end) => ({
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
});
// Use with for...of
for (const num of Range(5, 8)) {
console.log(num); // 5, 6, 7, 8
}
// Use with spread operator
const arr = [...Range(1, 5)];
console.log(arr); // [1, 2, 3, 4, 5]
// Use with Array.from
const typedArr = Array.from(Range(10, 13));
console.log(typedArr); // [10, 11, 12, 13]
This pattern is exactly how JavaScript’s built-in Set, Map, and array-like objects work.
Generator Functions
Generator functions provide a simpler way to create iterables. They use the function* syntax and automatically implement the iterator protocol:
function* countToThree() {
yield 1;
yield 2;
yield 3;
}
const generator = countToThree();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
console.log(generator.next().done); // true
Generators are iterable by default, so they work seamlessly with for...of:
function* fibonacciGenerator(limit) {
let a = 0, b = 1, count = 0;
while (count < limit) {
yield a;
[a, b] = [b, a + b];
count++;
}
}
for (const num of fibonacciGenerator(8)) {
console.log(num); // 0, 1, 1, 2, 3, 5, 8, 13
}
The yield keyword pauses execution and returns a value. When you call next() again, execution resumes from where it paused.
Infinite Iterables with Generators
Generators make it easy to create infinite sequences without consuming infinite memory:
function* naturalNumbers() {
let n = 1;
while (true) {
yield n++;
}
}
const numbers = naturalNumbers();
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3
// Take only what you need with for...of
for (const num of naturalNumbers()) {
if (num > 5) break;
console.log(num); // 1, 2, 3, 4, 5
}
The iteration only proceeds as far as needed, so the infinite generator doesn’t cause problems.
Built-in Iterables
Several JavaScript built-in types are iterable out of the box:
- Arrays and TypedArrays
- Strings (iterate over Unicode code points)
- Maps
- Sets
- Arguments objects
- DOM NodeLists and HTMLCollections
// Strings are iterable
for (const char of "hello") {
console.log(char); // h, e, l, l, o
}
// Maps are iterable (entries by default)
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
console.log(key, value); // a 1, b 2
}
// Sets are iterable
const set = new Set([1, 2, 3]);
for (const item of set) {
console.log(item); // 1, 2, 3
}
Common Iterator Methods
JavaScript provides built-in methods that work with iterables:
const arr = [1, 2, 3, 4, 5];
// Array.from converts iterables to arrays
const fromArray = Array.from(Range(1, 3));
console.log(fromArray); // [1, 2, 3]
// Spread operator works with iterables
const spreadArray = [...Range(1, 3)];
console.log(spreadArray); // [1, 2, 3]
// Destructuring works with iterables
const [first, second, ...rest] = Range(1, 5);
console.log(first, second, rest); // 1 2 [3, 4, 5]
// Array methods that return iterables
const mapped = [...arr.map(x => x * 2)];
console.log(mapped); // [2, 4, 6, 8, 10]
const filtered = [...arr.filter(x => x > 3)];
console.log(filtered); // [4, 5]
Summary
Symbols provide a way to create unique property keys, preventing accidental collisions in objects. Well-known symbols like Symbol.iterator let you customize how objects behave with language features.
The iterator protocol defines a standard way to produce sequences of values. Objects with a next() method returning { value, done } are iterators. The iterable protocol lets objects specify how they should be iterated using Symbol.iterator.
Together, these features enable for...of loops, the spread operator, destructuring, and many built-in JavaScript methods. Generator functions (function*) provide a convenient syntax for creating iterables without manually implementing the protocol.
These patterns appear throughout modern JavaScript—from arrays and strings to Maps, Sets, and custom data structures. Understanding them opens up new possibilities for creating flexible, iterable types in your applications.
In the next tutorial, you’ll explore classes and prototypes—JavaScript’s object-oriented programming features that build on these concepts.