Blog, COLDSURF

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; } }
ā† Go home