Back to Blog
JavaScriptSymbolIteratorsES6

JavaScript Symbol and Iterator Guide

Master JavaScript Symbols and iterators. From well-known symbols to custom iterables to generator protocols.

B
Bootspring Team
Engineering
June 26, 2020
7 min read

Symbols provide unique identifiers and enable powerful iteration patterns. Here's how to use them effectively.

Symbol Basics#

1// Creating symbols 2const sym1 = Symbol(); 3const sym2 = Symbol('description'); 4const sym3 = Symbol('description'); 5 6console.log(sym2 === sym3); // false - always unique 7 8// Symbols as property keys 9const id = Symbol('id'); 10 11const user = { 12 name: 'Alice', 13 [id]: 12345, 14}; 15 16console.log(user[id]); // 12345 17console.log(Object.keys(user)); // ['name'] - symbol not included 18 19// Symbol.for - global registry 20const globalSym = Symbol.for('app.id'); 21const sameSym = Symbol.for('app.id'); 22 23console.log(globalSym === sameSym); // true 24 25// Get key from global symbol 26console.log(Symbol.keyFor(globalSym)); // 'app.id' 27console.log(Symbol.keyFor(sym1)); // undefined (not in registry)

Well-Known Symbols#

1// Symbol.iterator - defines default iterator 2const iterable = { 3 data: [1, 2, 3], 4 5 [Symbol.iterator]() { 6 let index = 0; 7 const data = this.data; 8 9 return { 10 next() { 11 if (index < data.length) { 12 return { value: data[index++], done: false }; 13 } 14 return { done: true }; 15 }, 16 }; 17 }, 18}; 19 20console.log([...iterable]); // [1, 2, 3] 21 22// Symbol.toStringTag - customize Object.prototype.toString 23class MyClass { 24 get [Symbol.toStringTag]() { 25 return 'MyClass'; 26 } 27} 28 29console.log(Object.prototype.toString.call(new MyClass())); 30// '[object MyClass]' 31 32// Symbol.hasInstance - customize instanceof 33class MyArray { 34 static [Symbol.hasInstance](instance) { 35 return Array.isArray(instance); 36 } 37} 38 39console.log([] instanceof MyArray); // true 40 41// Symbol.toPrimitive - customize type coercion 42const obj = { 43 [Symbol.toPrimitive](hint) { 44 if (hint === 'number') return 42; 45 if (hint === 'string') return 'hello'; 46 return true; 47 }, 48}; 49 50console.log(+obj); // 42 51console.log(`${obj}`); // 'hello' 52console.log(obj + ''); // 'true'

Iterator Protocol#

1// Manual iterator 2const iterator = { 3 current: 0, 4 last: 5, 5 6 next() { 7 if (this.current <= this.last) { 8 return { value: this.current++, done: false }; 9 } 10 return { done: true }; 11 }, 12}; 13 14let result = iterator.next(); 15while (!result.done) { 16 console.log(result.value); // 0, 1, 2, 3, 4, 5 17 result = iterator.next(); 18} 19 20// Iterable object 21class Range { 22 constructor(start, end) { 23 this.start = start; 24 this.end = end; 25 } 26 27 [Symbol.iterator]() { 28 let current = this.start; 29 const end = this.end; 30 31 return { 32 next() { 33 if (current <= end) { 34 return { value: current++, done: false }; 35 } 36 return { done: true }; 37 }, 38 }; 39 } 40} 41 42const range = new Range(1, 5); 43console.log([...range]); // [1, 2, 3, 4, 5] 44 45for (const num of range) { 46 console.log(num); // 1, 2, 3, 4, 5 47}

Generator-Based Iterators#

1// Simpler iterator with generator 2class Range { 3 constructor(start, end) { 4 this.start = start; 5 this.end = end; 6 } 7 8 *[Symbol.iterator]() { 9 for (let i = this.start; i <= this.end; i++) { 10 yield i; 11 } 12 } 13} 14 15// Infinite iterator 16function* naturalNumbers() { 17 let n = 1; 18 while (true) { 19 yield n++; 20 } 21} 22 23// Take helper 24function* take(iterable, n) { 25 let count = 0; 26 for (const item of iterable) { 27 if (count >= n) return; 28 yield item; 29 count++; 30 } 31} 32 33console.log([...take(naturalNumbers(), 5)]); // [1, 2, 3, 4, 5] 34 35// Composable iterators 36function* map(iterable, fn) { 37 for (const item of iterable) { 38 yield fn(item); 39 } 40} 41 42function* filter(iterable, predicate) { 43 for (const item of iterable) { 44 if (predicate(item)) { 45 yield item; 46 } 47 } 48} 49 50const numbers = [1, 2, 3, 4, 5]; 51const result = [...filter(map(numbers, (x) => x * 2), (x) => x > 4)]; 52console.log(result); // [6, 8, 10]

Async Iterators#

1// Async iterator protocol 2const asyncIterable = { 3 async *[Symbol.asyncIterator]() { 4 for (let i = 0; i < 3; i++) { 5 await new Promise((r) => setTimeout(r, 100)); 6 yield i; 7 } 8 }, 9}; 10 11// Usage 12async function main() { 13 for await (const num of asyncIterable) { 14 console.log(num); // 0, 1, 2 (with delays) 15 } 16} 17 18// Fetch pages with async iterator 19async function* fetchPages(url) { 20 let page = 1; 21 let hasMore = true; 22 23 while (hasMore) { 24 const response = await fetch(`${url}?page=${page}`); 25 const data = await response.json(); 26 27 yield data.items; 28 29 hasMore = data.hasNextPage; 30 page++; 31 } 32} 33 34// Stream processing 35async function* readChunks(stream) { 36 const reader = stream.getReader(); 37 38 try { 39 while (true) { 40 const { done, value } = await reader.read(); 41 if (done) break; 42 yield value; 43 } 44 } finally { 45 reader.releaseLock(); 46 } 47}

Custom Collection Classes#

1// Linked list with iteration 2class LinkedList { 3 constructor() { 4 this.head = null; 5 this.tail = null; 6 this.size = 0; 7 } 8 9 add(value) { 10 const node = { value, next: null }; 11 12 if (!this.head) { 13 this.head = node; 14 this.tail = node; 15 } else { 16 this.tail.next = node; 17 this.tail = node; 18 } 19 20 this.size++; 21 } 22 23 *[Symbol.iterator]() { 24 let current = this.head; 25 26 while (current) { 27 yield current.value; 28 current = current.next; 29 } 30 } 31 32 get [Symbol.toStringTag]() { 33 return 'LinkedList'; 34 } 35} 36 37const list = new LinkedList(); 38list.add(1); 39list.add(2); 40list.add(3); 41 42console.log([...list]); // [1, 2, 3] 43console.log(Object.prototype.toString.call(list)); // '[object LinkedList]' 44 45// Tree iteration 46class BinaryTree { 47 constructor(value, left = null, right = null) { 48 this.value = value; 49 this.left = left; 50 this.right = right; 51 } 52 53 // In-order traversal 54 *[Symbol.iterator]() { 55 if (this.left) yield* this.left; 56 yield this.value; 57 if (this.right) yield* this.right; 58 } 59 60 // Pre-order 61 *preOrder() { 62 yield this.value; 63 if (this.left) yield* this.left.preOrder(); 64 if (this.right) yield* this.right.preOrder(); 65 } 66 67 // Post-order 68 *postOrder() { 69 if (this.left) yield* this.left.postOrder(); 70 if (this.right) yield* this.right.postOrder(); 71 yield this.value; 72 } 73} 74 75const tree = new BinaryTree( 76 2, 77 new BinaryTree(1), 78 new BinaryTree(3) 79); 80 81console.log([...tree]); // [1, 2, 3] (in-order) 82console.log([...tree.preOrder()]); // [2, 1, 3] 83console.log([...tree.postOrder()]); // [1, 3, 2]

Symbol Use Cases#

1// Private-like properties 2const _private = Symbol('private'); 3 4class MyClass { 5 constructor() { 6 this[_private] = 'secret'; 7 this.public = 'visible'; 8 } 9 10 getPrivate() { 11 return this[_private]; 12 } 13} 14 15const instance = new MyClass(); 16console.log(instance.public); // 'visible' 17console.log(instance[_private]); // Need the symbol reference 18console.log(Object.keys(instance)); // ['public'] 19 20// Method hooks 21const beforeSave = Symbol('beforeSave'); 22const afterSave = Symbol('afterSave'); 23 24class Model { 25 [beforeSave]() { 26 console.log('Before save'); 27 } 28 29 [afterSave]() { 30 console.log('After save'); 31 } 32 33 save() { 34 this[beforeSave](); 35 // Save logic 36 this[afterSave](); 37 } 38} 39 40// Type identification 41const type = Symbol('type'); 42 43class Cat { 44 static [type] = 'Cat'; 45 [type] = 'Cat'; 46} 47 48class Dog { 49 static [type] = 'Dog'; 50 [type] = 'Dog'; 51} 52 53function identify(animal) { 54 return animal[type]; 55} 56 57console.log(identify(new Cat())); // 'Cat' 58console.log(identify(new Dog())); // 'Dog'

Built-in Iterables#

1// Arrays 2const arr = [1, 2, 3]; 3for (const item of arr) console.log(item); 4 5// Strings 6const str = 'hello'; 7for (const char of str) console.log(char); 8 9// Maps 10const map = new Map([['a', 1], ['b', 2]]); 11for (const [key, value] of map) console.log(key, value); 12 13// Sets 14const set = new Set([1, 2, 3]); 15for (const item of set) console.log(item); 16 17// TypedArrays 18const typed = new Uint8Array([1, 2, 3]); 19for (const byte of typed) console.log(byte); 20 21// NodeList (DOM) 22const nodes = document.querySelectorAll('div'); 23for (const node of nodes) console.log(node); 24 25// arguments object 26function example() { 27 for (const arg of arguments) console.log(arg); 28}

Best Practices#

Symbols: ✓ Use for unique property keys ✓ Use Symbol.for for shared symbols ✓ Implement well-known symbols properly ✓ Document symbol purposes Iterators: ✓ Implement Symbol.iterator for collections ✓ Use generators for cleaner code ✓ Support for...of iteration ✓ Handle cleanup in finally Performance: ✓ Lazy evaluation with generators ✓ Avoid creating arrays when iterating ✓ Use built-in iteration methods ✓ Cache symbol references Avoid: ✗ Using symbols for true privacy ✗ Forgetting to close iterators ✗ Infinite iterators without limits ✗ Over-engineering iteration

Conclusion#

Symbols provide guaranteed unique identifiers and enable powerful meta-programming through well-known symbols. The iterator protocol enables custom iteration with for...of, spread, and destructuring. Use generators for simpler iterator implementation and async iterators for asynchronous data streams.

Share this article

Help spread the word about Bootspring