Nick Ribal
Front-end veteran, consultant and freelancer, who's armed with a decade of experience solving challenges on the Web platform for large Internet companies.
Know what you're iterating
Based on that, decide how to iterate
Use collection implementation details to iterate
// 1. Array and TypedArray
const array = ['this', 'is', 'aweful']
array.forEach(member => console.log(member))
// 3. Newer ES2015 collections
for (member of new Set(array)) console.log(member)
// 2. Array-like: arguments, NodeList, String, etc
(function(){
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i])
}
}).apply(null, array)
// Some things aren't what they seem
for (let i = 0; i < '😀'.length; i++)
console.log('😀'[i]) // � � Unicode in JS
const array = ['😀', '😀']
function getCollections(){
return [
'😀',
array,
arguments,
new Set(['😀']),
new Map([array])
]
}
for (const collection of getCollections()) {
for (const member of collection) {
console.log(member)
}
}
Object with [Symbol.iterator] property (own or in prototype chain), whose value is a function, which returns an Iterator
String, Array, TypedArray, Map, Set, arguments, NodeList (DOM collections), generator object
// In TypeScript notation
interface Iterable {
[Symbol.iterator](): Iterator;
}
iterator.next() can be called to request a value
next() will return IteratorResult object
iterator.return() is called when iteration terminates by break, continue, return or throw
returns { value, done: true }
interface Iterator {
next(): IteratorResult;
return?(value?): IteratorResult;
}
{ value, done: false }
as long as iterator has values
return() or after iterator exhaustion should return
{ done: true }
interface IteratorResult {
value: any;
done: boolean;
}
{ done: true }
after last value
A protocol defines interfaces (signatures for methods or functions) and rules for using them
interface Iterable {
[Symbol.iterator](): {
// Iterator
next(): {
// IteratorResult
value: any;
done: boolean;
};
return?(value?): {
// IteratorResult
value: any;
done: boolean;
};
};
}
Iterable has Iterator, which repeatedly returns IteratorResult(s)
const iterable = ['😀', '😀😀', '😀😀😀']
// for-of loop
for (const member of iterable)
console.log(member)
// destructuring assignment
const [ first, second, ] = iterable
// array spread operator
const [ firstAgain, ...secondAndOn] = iterable
// recursive yield in generator
function* generator(){ yield* iterable }
for (const member of generator())
console.log(member)
const plan1 = { // Iterable
[Symbol.iterator](){
let step = 0
const steps = [ // IteratorResult, not done
{ done: false, value: '1. Learn iterables!' },
{ done: false, value: '2. Learn generators' },
{ done: false, value: '3. ????????????????' },
{ done: false, value: '4. PROFIT!!!!!!!!!!' }
]
return { // Iterator
next: () => steps[step++] || { done: true }
}
}
}
for (const step of plan1)
console.log(step)
1. Learn iterables!
2. Learn generators
3. ????????????????
4. PROFIT!!!!!!!!!!
const plan2 = (function(step = 0){
const steps = [ // IteratorResult
{ done: false, value: '1. Learn iterables!' },
{ done: false, value: '2. Learn generators' },
{ done: false, value: '3. ????????????????' },
{ done: false, value: '4. PROFIT!!!!!!!!!!' }
]
return {
[Symbol.iterator](){ return this }, // Iterable
next(){ // Iterator
const isDone = step >= steps.length // exhaustion
return isDone ? { done: true } : steps[step++]
}
}
})()
// a for-of loop receives only the value
for (const step of plan2) console.log(step)
for (const step of plan2) {
console.log(step)
break;
}
// > 1. Learn iterables!
for (const step of plan2) {
console.log(step)
}
// > 2. Learn generators
// > 3. ????????????????
// > 4. PROFIT!!!!!!!!!!
const plan3 = (function(step = 0){
const steps = [
'1. Learn iterables!', '2. Learn generators',
'3. ????????????????', '4. PROFIT!!!!!!!!!!',
], { length: len, } = steps
return {
[Symbol.iterator](){ return this },
next: () => ({ value: steps[step++], done: step > len }),
return(){
step = len + 1 // cleanup here
return { done: true }
}
}
})()
for (const step of plan3) {
console.log(step)
break // or continue or throw or return
}
// > 1. Learn iterables!
for (const step of plan3) console.log(step)
// return() closed iterator, so no more values
Generators are functions which can be exited and later re-entered. Their variables are saved across re-entrances.
Calling a generator function does not execute it, but returns an iterator for that generator
I'm back - Iterator
function* generatorFnDeclaration(){}
const generatorFnExpression = function*(){}
const objectLiteralWithGeneratorMethod = {
* genratorMethod(){}
}
class ClassWithGeneratorMethods {
* instanceGeneratorMethod(){}
static * classGeneratorMethod(){}
}
const { log, } = console
function* generator(){
log('before yield')
yield 'yielded value'
log('after yield')
}
const iterator = generator()
for (const value of iterator)
log(value)
'before yield'
'yielded value'
'after yield'
yield causes the generator function to pause and returns the yielded value to the calling iterator's next() as the return value, wrapped in an IteratorResult object
function* generator(){ yield 'foo' }
const iterator = generator()
console.log(iterator.next()) // { value: 'foo', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
console.log(iterator.next()) // { value: undefined, done: true }
A paused generator is resumed by calling iterator's next().
Each next() call, resumes generator execution and runs until nearest yield/throw/return or end of generator function
const { log, } = console
function* generator(){
log('G: before yield')
yield 'G: yielded value'
log('G: after yield')
}
const iterator = generator()
log(iterator.next())
// > 'G: before yield'
// > { value: 'G: yielded value', done: false }
log(iterator.next())
// > 'G: after yield'
// > { value: undefined, done: true }
log(iterator.next())
// > {
// > value: undefined,
// > done: true
// > }
log(iterator.next())
// > {
// > value: undefined,
// > done: true
// > }
No output cause only the controlling iterator's next() call runs the generator till following yield
generator pauses after yielding
const { log, } = console
function* generator(){
log('G: before yield')
log('G: after yield', yield 'G: yielded value')
}
const iterator = generator()
for (const value of iterator)
log(value)
'G: before yield'
'G: yielded value'
'G: after yield' undefined
iterator's next()...
...is next(undefined)
const { log, } = console
function* generator(){
log('G: before yield')
const valueFromNext = yield 'G: yielded value'
log('G: after yield', valueFromNext)
}
const iterator = generator()
log(iterator.next('I: 1'))
// > 'G: before yield'
// > { value: 'G: yielded value', done: false }
log(iterator.next('I: 2'))
// > 'G: after yield' 'I: 2'
// > { value: undefined, done: true }
log(iterator.next('I: 3'))
// > {
// > value: undefined,
// > done: true
// > }
log(iterator.next('I: 4'))
// > {
// > value: undefined,
// > done: true
// > }
No output cause only the controlling iterator's next() call runs the generator till following yield
generator pauses after yielding
const { log, } = console
function* generator(){
log('G: before yield')
const valueFromNext = yield 'G: yielded value'
log('G: after yield', valueFromNext)
}
const iterator = generator()
log(iterator.next('I: 1'))
// > 'G: before yield'
// > { value: 'G: yielded value', done: false }
log(iterator.next('I: 2'))
// > 'G: after yield' 'I: 2'
// > { value: undefined, done: true }
sent...
sent...
...recieved
???
const { log } = console
function* generator(){
log('G: before yield')
const valueFromNext = yield 'G: yielded value'
log('G: after yield', valueFromNext)
}
const iterator = generator()
log(iterator.next('I: 1'))
// > 'G: before yield'
// > { value: 'G: yielded value', done: false }
log(iterator.next('I: 2'))
// > 'G: after yield' 'I: 2'
// > { value: undefined, done: true }
function* series(step = 1, start = 0){
while (true) yield start += step
}
const naturalNumbers = series()
// iterables are lazy, so this is cool
const [one, two, ] = naturalNumbers
// but these cause a stack overflow
const [f, s, ...rest, ] = naturalNumbers
for (const n of naturalNumbers) {
// without a break/continue/throw
}
function objectEntries(obj){
let index = 0 // Reflect.ownKeys() gets strings/symbols
const propKeys = Reflect.ownKeys(obj)
return {
[Symbol.iterator](){ return this },
next(){
if (index < propKeys.length) {
const key = propKeys[index++]
return {
value: [key, obj[key]]
}
}
return { done: true }
}
}
}
Initialization
Iteration
Exhaustion
function* objectEntries(obj){
for (const key of Reflect.ownKeys(obj)) {
yield [key, obj[key]]
}
}
function map(obj, fn, result = {}){
for (const [key, value] of objectEntries(obj)) {
result[key] = fn(value)
}
return result
}
map({ foo: 2, bar: 5 }, x => x**2) // ES2016 FTW!!!
// { foo: 4, bar: 25 }
function* recursive(){
yield 'sequence'
yield* ['of', 'yielded']
yield 'values'
}
const series = recursive()
for (const i of series) {
console.log(i)
}
// > "sequence"
// > "of"
// > "yielded"
// > "values"
function* flat(){
yield 'sequence'
yield ['of', 'yielded']
yield 'values'
}
const series = flat()
for (const i of series) {
console.log(i)
}
// > "sequence"
// > ["of", "yielded"]
// > "values"
function* g2(){
yield 2
yield* [3, 4]
yield* '5😀'
}
function* g1(){
yield '1 start'
yield* g2()
yield '7 end'
}
for (const i of g1()) console.log(i)
'1 start'
2
3
4
'5'
'😀'
'7 end'
By Nick Ribal
Iterable, iterator, generator, function*, yield, yield*, spread operator, destructuring, for-of and Symbol.iterable are commonly used by ES2015 built-ins. Learn why and how these related concepts come together to implement ES2015's advanced features - so you too can harness their power!
Front-end veteran, consultant and freelancer, who's armed with a decade of experience solving challenges on the Web platform for large Internet companies.