Iterables
- A value in a
for of
loop must be iterable:
for (element of [1, 2, 3])
for (ch of 'Hello')
- A value is iterable if it has a property with key
Symbol.iterator
'Hello'[Symbol.iterator] → [Function: [Symbol.iterator]]
- That's a constructor function for an iterator object:
const helloIter = 'Hello'[Symbol.iterator]()
- That object has a
next
method:
helloIter.next() → { value: 'H', done: false }
helloIter.next() → { value: 'e', done: false }
...
helloIter.next() → { value: 'o', done: false }
helloIter.next() → { value: undefined, done: true }
for (const value of iterable)
- Array spread:
[...iterable]
- Array destructuring:
[first, second, third] = iterable
Array.from(iterable)
- Set and map constructor:
new Set(iterable)
yield*
- Any place where a programmer makes use of the iterator constructed by
obj[Symbol.iterable]
- Arrays
- Strings
- Sets and maps
- The objects returned by the
keys
, values
, entries
methods of arrays, typed arrays, sets, and maps (but not Object
)
- DOM data structures
- Open any web page and run
[...document.querySelectorAll('div')]
in the console.
- Any value where a programmer provides a constructor for an iterator with the
Symbol.iterable
key.
Iterators
Implementing an Iterable
next: () => {
...
if (...) return { value: some value, done: false }
else return { value: undefined, done: true }
}
Can omit done: false
and value: undefined
Example: Range
class Range {
constructor(from, to) {
this.from = from
this.to = to
}
[Symbol.iterator]() {
let current = this.from
return {
next: () => {
if (current < this.to) {
const result = { value: current }
current++
return result
}
else
return { done: true }
}
}
}
}
Closeable Iterators
Closeable Iterator Implementation
function lines(filename) {
const file = ... // Open the file
return {
[Symbol.iterator]: () => ({
next: () => {
if (done) {
... // Close the file
return { done: true }
}
else {
const line = ... // Read a line
return { value: line }
}
},
['return']: () => {
... // Close the file
return { done: true } // Must return an object
}
})
}
}
Generators
Generators
- Tedious to write iterators that yield one value with each call to
next
- Generators produce all values in a generator function.
- Function is suspended after each yielded value.
function* rangeGenerator(from, to) {
for (let i = from; i < to; i++)
yield i
}
*
tags this function as a generator function.
- Call the function to obtain an iterator:
const rangeIter = rangeGenerator(1, 10)
- It really is an iterator:
rangeIter.next() → { value: 1, done: false }
rangeIter.next() → { value: 2, done: false }
...
rangeIter.next() → { value: 9, done: false }
rangeIter.next() → { value: undefined, done: true }
Generator Function Syntax
- Named or anonymous function:
function* myGenerator(...)
const myGenerator = function* (...)
- Object literal:
{ * myGenerator(...) { ... }, ... }
// Syntactic sugar for myGenerator: function* (...) {...}
- Method in class:
class MyClass {
* myGenerator(...) { ... }
...
}
- Arrow functions cannot be generators.
Execution Flow
Nested Yield
- It is easy to yield all elements of an array:
function* arrayGenerator(arr) {
for (let element of arr)
yield element
}
- What if one of the elements is itself an array? The
yield*
operator yields its elements one at a time:
function* flatArrayGenerator(arr) {
for (let element of arr)
if (Array.isArray(element))
yield* element
else
yield element
}
let g = flatArrayGenerator([1, [2, [3, 4], 5], 6])
[...g] → [1, 2, [3, 4], 5, 6]
Nested Yield
- So close...
- This works for arbitrary levels of nesting:
function* reallyFlatArrayGenerator(arr) {
for (let element of arr)
if (Array.isArray(element))
yield* reallyFlatArrayGenerator(element)
else
yield element
}
g = reallyFlatArrayGenerator([1, [2, [3, 4], 5], 6])
[...g] → [1, 2, 3, 4, 5, 6]
- Recursive nested generators make it easy to traverse nested data structures.
Generators as Consumers
End of Lesson 9