Javascript Iterable
This is a translation based on the article from https://helloworldjavascript.net/pages/260-iteration.html.
Iterable
The
iterable
object was introduced in ES2015 with the for...of
loop.The feature that distinguishes iterable objects from other objects is that they contain a special type of function in their
Symbol.iterator
property.const str = 'hello'; str[Symbol.iterator]; // [Function]
If an object has a function in its
Symbol.iterator
property, it is called an iterable object (or simply iterable), and we say the object satisfies the iterable protocol. These objects can use various features added in ES2015.Some of the built-in constructors that create iterable objects include:
String
Array
TypedArray
Map
Set
Using Iterables
If an object is iterable, you can use the following features on it:
for...of
loop
- Spread operator (
...
)
- Destructuring assignment
- Other functions that accept iterables as arguments
In other words, you can use these features with strings as well. Run the following code and see the results for yourself:
// `for...of` for (let c of 'hello') { console.log(c); } // Spread operator const characters = [...'hello']; // Destructuring assignment const [c1, c2] = 'hello'; // `Array.from` accepts iterable or array-like objects as arguments. Array.from('hello');
Generator Functions
The easiest way to implement an iterable is by using generator functions, introduced in ES2015.
A generator function is a special type of function that returns an iterable object.
// Function declaration function* gen1() {} // Function expression const gen2 = function* () {} // Method syntax const obj = { * gen3() {} }
When you call a generator function, an object is created that satisfies the iterable protocol. In other words, it has a
Symbol.iterator
property.function* gen1() {} const iterable = gen1(); iterable[Symbol.iterator]; // [Function]
Inside a generator function, you can use the special
yield
keyword. The yield
keyword works similarly to return
, but instead of terminating the function, it yields values from the generator. Values following the yield
keyword are returned in order when the iterable is used.function* numberGen() { yield 1; yield 2; yield 3; } // 1, 2, 3 will be logged in order. for (let n of numberGen()) { console.log(n); }
Using
yield*
, you can pass values from another generator function.function* numberGen() { yield 1; yield 2; yield 3; } function* numberGen2() { yield* numberGen(); yield* numberGen(); } // 1, 2, 3, 1, 2, 3 will be logged in order. for (let n of numberGen2()) { console.log(n); }
There are a few things to keep in mind when using generator functions:
- The iterable created from a generator function can only be used once.
- You cannot use the
yield
keyword in a normal function defined inside a generator function.
// The iterable created from a generator function can only be used once. function* gen() { yield 1; yield 2; yield 3; } const iter = gen(); for (let n of iter) { // Works fine. console.log(n); } for (let n of iter) { // `iter` was used once, so this will not execute. console.log(n); }
// You cannot use the `yield` keyword in a normal function inside a generator function. function* gen2() { // This causes a syntax error (Unexpected token). function fakeGen() { yield 1; yield 2; yield 3; } fakeGen(); }
Iterator Protocol
It's important to distinguish between iterable and iterator.
An iterable object satisfies the iterable protocol, meaning it has a
Symbol.iterator
property containing a special function.For an object to satisfy the Iterable Protocol, the function stored in the
Symbol.iterator
property must return an iterator object.An iterator object must meet the following conditions:
- The iterator must have a
next
method.
- The
next
method must return an object with two properties: done
: Indicates whether the iteration is complete.value
: The current value in the sequence.
These conditions define the Iterator Protocol.
const strIterator = 'go'[Symbol.iterator](); strIterator.next(); // { value: 'g', done: false } strIterator.next(); // { value: 'o', done: false } strIterator.next(); // { value: undefined, done: true } strIterator.next(); // { value: undefined, done: true } function* gen() { yield 1; yield 2; } const genIterator = gen()[Symbol.iterator](); genIterator.next(); // { value: 1, done: false } genIterator.next(); // { value: 2, done: false } genIterator.next(); // { value: undefined, done: true } genIterator.next(); // { value: undefined, done: true }
Creating an iterable:
function range(start = 0, end = Infinity, step = 1) { return { currentValue: start, [Symbol.iterator]() { return { next: () => { if (this.currentValue < end) { const value = this.currentValue; this.currentValue += step; return { done: false, value } } else { return { done: true } } } } } } }
Generator and Iterator
Objects created from generator functions behave like normal iterables but have special properties related to iterators.
Objects created from a generator function satisfy both the iterable protocol and the iterator protocol.
function* gen() { // ... } const genObj = gen(); genObj[Symbol.iterator]().next === genObj.next; // true
You can call
next
directly on the object without creating an iterator using Symbol.iterator
.function* gen() { yield 1; yield 2; yield 3; } const iter = gen(); iter.next(); // { value: 1, done: false } iter.next(); // { value: 2, done: false } iter.next(); // { value: 3, done: false } iter.next(); // { value: undefined, done: true }
If you use the
return
keyword inside a generator function, the iteration ends immediately, and the returned value is stored in the properties of the object returned by next
. However, the returned value will not be included in the iteration process.function* gen() { yield 1; return 2; // The generator function ends here. yield 3; } const iter = gen(); iter.next(); // { value: 1, done: false } iter.next(); // { value: 2, done: true } iter.next(); // { value: undefined, done: true } // Only `1` is printed. for (let v of gen()) { console.log(v); }
You can pass arguments to the
next
method of a generator function, and the value returned by the yield
expression where the function paused will be replaced by the argument.function* gen() { const received = yield 1; console.log(received); } const iter = gen(); iter.next(); // { value: 1, done: false } // 'hello' will be printed. iter.next('hello'); // { value: undefined, done: true }
Generator Examples
// Mapping each item before yielding function* map(iterable, mapper) { for (let item of iterable) { yield mapper(item); } } // Yielding the accumulated value up to each step function* reduce(iterable, reducer, initial) { let acc = initial; for (let item of iterable) { acc = reducer(acc, item); yield acc; } } // Yielding only items that satisfy a condition function* filter(iterable, predicate) { for (let item of iterable) { if (predicate(item)) { yield item; } } } // Concatenating multiple iterables function* concat(iterables) { for (let iterable of iterables) { yield* iterable; } } // Yielding only the first few items function* take(iterable, count = Infinity) { const iterator = iterable[Symbol.iterator](); for (let i = 0; i < count; i++) { const {value, done} = iterator.next(); if (done) break; yield value; } }