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.