Translation source: exploringjs.com/es6/ch_iter…
21 Iterables and iterators
21.1 an overview of the
ES6 introduces a new mechanism for traversing data: iteration. Two concepts are at the heart of iteration:
- Iterable is a data structure that allows easy access to its elements. It does this by implementing a key of
Symbol.iterator
To achieve. This is the factory method for iterators. - Iterators are Pointers that are used to traverse data structure elements (think cursors in a database).
Here’s how interfaces are represented in TypeScript:
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
}
interface IteratorResult {
value: any;
done: boolean;
}
Copy the code
21.1.1 Iterable Objects
Here are a few:
- Arrays
- Strings
- Maps
- Sets
- DOM data structures (work in progress)
Literal objects are non-iterable, as explained below.
21.1.2 The internal construction is iterative
-
Destruct by array pattern:
const [ a , b ] = new Set ([ 'a' , 'b' , 'c' ]); Copy the code
-
The for – cycle:
for ( const [ 'a' , 'b' , 'c' ]) { console . log ( x ); } Copy the code
-
Array. The from () :
const arr = Array . from ( new Set ([ 'a' , 'b' , 'c' ])); Copy the code
-
Expansion operator (…) :
const arr = [... new Set ([ 'a' , 'b' , 'c' ])]; Copy the code
-
Constructors for Maps and Sets:
const map = new Map ([[ false , 'no' ], [ true , 'yes' ]]); const set = new Set ([ 'a' , 'b' , 'c' ]); Copy the code
-
Promise. All (), Promise. Race () :
Promise.all (iterableOverPromises). Then (···);Promise.race (iterableOverPromises). Then (···);Copy the code
-
Yield * :
yield * anIterable ; Copy the code
21.2 Iterability
The main concepts of iterability are as follows:
- Data consumers :JavaScript has a language structure that uses Data. For example,
for-of
Loop over the value, while the spread operator (...
Inserts a value into an array or function call. - Data sources: Data consumers can obtain their values from a variety of sources. For example, you might want to iterate over elements of an array, key-value entries in a Map, or characters in a string.
It is impractical for every consumer to support all sources, especially since new sources can be created (for example through libraries). A unified interface mechanism is needed to handle all the different data structures. Thus, ES6 introduced Iterable. Data consumers use it, data sources implement it:
Because there are no interfaces in JS, iterators are more of a convention. Provides a unified access mechanism for various data structures. The Iterator interface can be deployed on any data structure to complete traversal (that is, processing all members of the data structure in turn).
- Source: If the key of a method of a value is a symbol
Symbol.iterator
, which returns what is called an iterator, and the value is considered iterable. An iterator is a method that passes through it,
Next () ‘returns the value of the object. We say: it iterates over iterable items (content), returning one value per call. - Consumption: Data consumers use iterators to retrieve the values they are using.
Now, how can array ARR be consumed? First create an iterator using Symbol. Iterator:
const arr = ['a'.'b'.'c'];
const iter = arr[Symbol.iterator]();
Copy the code
Each item in the array is then repeatedly retrieved via the iterator’s next() method:
> iter.next()
{ value: 'a'.done: false }
> iter.next()
{ value: 'b'.done: false }
> iter.next()
{ value: 'c'.done: false }
> iter.next()
{ value: undefined.done: true }
Copy the code
As you can see, each item returned by next() is wrapped in an object whose value is the value of the item in the original array, and whether done has completed the retrieval of the array item sequence.
Iterable and iterators are part of what is called an iterative protocol (interfaces plus rules that use them). A key feature of this protocol is that it is sequential: iterators return one value at a time. This means that if an iterable data structure is nonlinear (such as a tree), iteration will linearize it.
21.3 Iterable data sources
I’ll use for-of loops (see section for-of Loops) to iterate over various iterable data.
21.3.1 array
Typed Arrays can iterate over its elements:
for ( const [ 'a' , 'b' ]) {
console . log ( x );
}
// Output:
// 'a'
// 'b'
Copy the code
21.3.2 string
Strings are iterable, but they traverse Unicode code points, each of which may contain one or two JavaScript characters:
for (const x of 'a\uD83D\uDC0A') {
console.log(x);
}
// Output:
// 'a'
// '\uD83D\uDC0A' (crocodile emoji)
Copy the code
You just saw that original values can also be iterated. So one doesn’t have to be an object to be iterable. This is because all values are cast to objects before accessing the iterator method (the property key symbol.iterator).
21.3.3 Maps
A map is an iteration of its entries. Each entry is encoded as a [key, value] pair, with a two-element Array. These items are always iterated in a deterministic manner, in the same order as when they were added to the map.
const map = new Map().set('a'.1).set('b'.2);
for (const pair of map) {
console.log(pair);
}
// Output:
// ['a', 1]
// ['b', 2]
Copy the code
Note that WeakMaps are not iterative.
21.3.4 Sets.
A collection is an iteration of its elements (in the same order that they were added to the collection).
const set = new Set().add('a').add('b');
for (const x of set) {
console.log(x);
}
// Output:
// 'a'
// 'b'
// 'b'
Copy the code
Note that WeakSets are not iterative.
21.3.5 arguments
Although the special variable arguments is more or less obsolete in ECMAScript 6 (due to rest arguments), it is iterable:
function printArgs() {
for (const x of arguments) {
console.log(x);
}
}
printArgs('a'.'b');
// Output:
// 'a'
// 'b'
Copy the code
21.3.6 DOM Data Structure
Most DOM data structures are ultimately iterable:
for (const node of document.querySelectorAll('div'{...}))Copy the code
Please note that implementing this feature is a work in progress. But doing so is relatively easy because the Symbol. Iterator does not conflict with existing property keys.
21.3.7 Variable computing data
Not all iterable content has to come from a data structure; it can also be computed in real time. For example, all major ES6 data structures (Arrays, Typed Arrays, Maps, Sets) have three methods for returning iterable objects:
entries()
Returns an iterable entry, Array encoded as [key, value]. For Arrays, the values are Array elements and the keys are their indexes. For collections, each key and value is the same -set element.keys()
Returns the iterable value of the entry key.values()
Returns the iterable value of the item value.
Let’s see what it looks like. Entries () gives you a good way to get Array elements and their indexes:
const arr = ['a'.'b'.'c'];
for (const pair of arr.entries()) {
console.log(pair);
}
// Output:
// [0, 'a']
// [1, 'b']
// [2, 'c']
Copy the code
21.3.8 Common Objects Cannot be Iterated
Ordinary objects (created by object literals) are not iterable:
for (const x of{{})// TypeError
console.log(x);
}
Copy the code
Why can’t objects iterate over properties by default? The reasoning is as follows. You can iterate through two levels in JavaScript:
- Program level: Iterating attributes means checking the structure of the program.
- Data level: Iterating over data structures means examining the data managed by the program.
Iterating over attributes by default means mixing these levels, which has two disadvantages:
- You cannot iterate over properties of a data structure.
- Turning an object into a data structure after iterating over its properties breaks your code.
There is also a warning if the engine iterates through the object.prototype [symbol.iterator]() method: Objects created with Object.create(null) will not be iterable because Object.Prototype is not in their prototype chain.
It is important to remember that if objects are used as Maps, the properties of iterating objects are mostly interesting. But we only did this in ES5, when we had no better choice. In ECMAScript 6, we have a built-in data structure called Map.
21.3.8.1 How do I Iterate attributes
The correct (and safe) way to iterate over attributes is through utility functions. For example, with objectEntries(), its implementation will be shown later (a future version of ECMAScript might have something similar built in) :
const obj = { first: 'Jane'.last: 'Doe' };
for (const [key,value] of objectEntries(obj)) {
console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe
Copy the code
21.4 Iterative language structure
The following ES6 language constructs use the iteration protocol:
- Destruct through array patterns
for-of
cycleArray.from()
- Expansion operator (
.
) - Constructor for Maps and Sets
Promise.all()
.Promise.race()
yield*
Details are covered in the following sections
21.4.1 Deconstruction through array pattern
Destructuring through array patterns applies to any iterable:
const set = new Set().add('a').add('b').add('c');
const [x,y] = set;
// x='a'; y='b'
const [first, ...rest] = set;
// first='a'; rest=['b','c'];
Copy the code
21.4.2 for – cycle
For-of is a new loop in ECMAScript 6. Its basic form is as follows:
for(const x of iterable) {···}Copy the code
For more information, see for-of loops.
Note that iterable’s iterability is required otherwise for-of cannot loop through the values. This means that non-iterable values must be converted to iterable values. For example, through array.from ().
21.4.3 Array.from()
Array.from() converts iterable and array-like values to Arrays. It also works with typed Arrays.
> Array.from(new Map().set(false.'no').set(true.'yes'[[))false.'no'], [true.'yes']]
> Array.from({ length: 2.0: 'hello'.1: 'world' })
['hello'.'world']
Copy the code
For more information about array.from (), see the section on arrays.
21.4.4 Expanding Operators (…)
The spread operator inserts the iterable value into an Array:
> const arr = ['b'.'c'];
> ['a'. arr,'d']
['a'.'b'.'c'.'d']
Copy the code
This means that it gives you an easy way to convert any iteration to an array:
const arr = [... iterable ];
Copy the code
The expansion operator also converts iterable to arguments to a function, method, or constructor call:
> Math.max(... [- 1.8.3])
8
Copy the code
21.4.5 Maps and Sets constructors
The Map constructor changes the iterable on [key, value] to a Map:
> const map = new Map([['uno'.'one'], ['dos'.'two']]);
> map.get('uno')
'one'
> map.get('dos')
'two'
Copy the code
The constructor of a Set converts an iterable element to a Set:
> const set = new Set(['red'.'green'.'blue']);
> set.has('red')
true
> set.has('yellow')
false
Copy the code
WeakMap and WeakSet constructors work in a similar way. In addition, Maps and Sets themselves are iterable (WeakMaps and WeakSets are not), which means you can clone them using their constructors.
21.4.6 Promises
Promise.all() and promise.race () accept the iteration on Promises:
Promise. All (iterableOverPromises). Then (...);Promise. Race (iterableOverPromises). Then (...);Copy the code
21.4.7 yield*
Yield * is an operator that is only available within the generator. It produces all the items iterated over by the iterator.
function* yieldAllValuesOf(可迭代) {
yield* iterable;
}
Copy the code
Yield * The most important use case is the recursive call generator (which produces something iterable).
21.5 Implementation of Iteration
In this section, I’ll explain in detail how to implement Iterables. Note that ES6 generators are generally easier to implement than “manual.”
The iteration protocol is as follows:
An object becomes Iterable (” implements “Iterable) if it has a method (own or inherited) whose key is symbol. iterator. The method must return an iterator, an object that iterates through the “inner” item of its method next().
In TypeScript representations, iterables and iterators interface like this:
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
return? (value? : any) : IteratorResult; } interface IteratorResult {value: any;
done: boolean;
}
Copy the code
Return () is an optional method that we will use later. Let’s first implement a simulated iteration to see how iteration works.
const iterable = {
[Symbol.iterator]() {
let step = 0
const iterator = {
next() {
if (step <= 2) {
step++
}
switch (step) {
case 1:
return { value: 'hello'.done: false }
case 2:
return { value: 'world'.done: false }
default:
return { value: undefined.done: true}}}}return iterator
}
}
Copy the code
Let’s check that iterable is actually iterable:
for (const x of iterable) {
console.log(x)
}
// Output:
// hello
// world
Copy the code
The code executes three steps, and the counter step ensures that everything happens in the correct order. First, we return the value ‘hello’, then the value ‘world’, and then we indicate that the iteration has ended. Each item is contained in an object with the following properties:
value
Save the actual valuedone
Is a Boolean flag indicating whether the destination has been reached
If false, done can be omitted; If undefined, value can be omitted. In other words, the switch statement can be written as follows.
switch (step) {
case 1:
return { value: 'hello' }
case 2:
return { value: 'world' }
default:
return { done: true}}Copy the code
As explained in the generators section, in some cases you even need the last done: true to get value. Otherwise, next() could be simpler and return items directly (without wrapping them in objects). The end of the iteration is then indicated by a special value (for example, a symbol).
Let’s look at one more iterative implementation. IterateOver () returns an iterable from the arguments passed to it:
function iterateOver(. args) {
let index = 0
const iterable = {
[Symbol.iterator]() {
const iterator = {
next() {
if (index < args.length) {
return { value: args[index++] }
} else {
return { done: true}}}}return iterator
}
}
return iterable
}
// Using `iterateOver()`:
for (const x of iterateOver('fee'.'fi'.'fo'.'fum')) {
console.log(x)
}
// Output:
// fee
// fi
// fo
// fum
Copy the code
21.5.1 Iterable iterators
If the iterable and iterator are the same object, we can simplify the previous function:
function iterateOver(. args) {
let index = 0
const iterable = {
[Symbol.iterator]() {
return this
},
next() {
if (index < args.length) {
return { value: args[index++] }
} else {
return { done: true}}}}return iterable
}
Copy the code
Even if the original iterable and iterator are not the same object, it can occasionally be useful if the iterator has the following methods (which also make it iterable) :
[Symbol.iterator]() {
return this;
}
Copy the code
All built-in ES6 iterators follow this pattern (through a common prototype, see the section on generators). For example, the default iterator for Arrays:
> const arr = [];
> const iterator = arr[Symbol.iterator]();
> iterator[Symbol.iterator]() === iterator
> true
Copy the code
What is the use of an iterator if it is also iterable? For-of applies only to iterables, not iterators. Because Array iterators are iterable, we can continue iterating in another loop:
const arr = ['a'.'b']
const iterator = arr[Symbol.iterator]()
for (const x of iterator) {
console.log(x) // a
break
}
// Continue with same iterator:
for (const x of iterator) {
console.log(x) // b
}
Copy the code
One use case to continue iterating is that you can remove initial items (such as headings) before processing the actual content through for-of.
21.5.2 Optional iterator methods:return()
andthrow()
Two iterator methods are optional:
- If the iteration ends prematurely, then
return()
Provide opportunities for iterators to clean up. throw()
It’s about forwarding the method call to passyield*
An iterative generator. The relevantChapter on GeneratorsThis is explained.
21.5.2.1 throughreturn()
Close iterator
As mentioned earlier, the optional iterator method return() is about letting the iterator clean up if the iterator does not iterate until the end. It closes an iterator. In for-of loops, premature (or sudden, in canonical languages) termination can be caused by:
- break
- Continue (if you continue the external loop
continue
The action is similar tobreak
) - throw
- return
In each case, for-of lets the iterator know that the loop will not complete. Let’s look at an example, a function readLinesSync that returns an iterable line of text from a file and wants to close the file no matter what happens:
function readLinesSync(fileName) {
constThe file =...return{{... next ()if (file.isAtEndOfFile()) {
file.close();
return { done: true}; }...},return() {
file.close();
return { done: true}; }}; }Copy the code
Due to return(), the file will be closed correctly in the following loop:
// Only print first line
for (const line of readLinesSync(fileName)) {
console.log(x)
break
}
Copy the code
The return() method must return an object. This is due to the way the generator handles return statements, as explained in the section on generators.
The following constructs close iterators that are not completely “exhausted” :
- for-of
- yield*
- Destructuring
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()
- Promise.all(), Promise.race()
A later section will provide more information on closing iterators.
21.6 More examples of iterable
In this section, we’ll look at some examples of iterations. Most of these iterations are easier to implement through generators. The chapter on generators shows how to do this.
21.6.1 Utility functions that return iterables
Returning iterable utility functions and methods is just as important as iterable data structures. The following utility functions are used to iterate over the object’s own properties.
function objectEntries(obj) {
let index = 0
// In ES6, you can use strings or symbols as property keys,
// Reflect.ownKeys() retrieves both
const propKeys = Reflect.ownKeys(obj)
return{[Symbol.iterator]() {
return this
},
next() {
if (index < propKeys.length) {
const key = propKeys[index]
index++
return { value: [key, obj[key]] }
} else {
return { done: true }
}
}
}
}
const obj = { first: 'Jane'.last: 'Doe' }
for (const [key, value] of objectEntries(obj)) {
console.log(`${key}: ${value}`)}// Output:
// first: Jane
// last: Doe
Copy the code
Another option is to use iterators instead of indexes to iterate over arrays with attribute keys:
function objectEntries(obj) {
let iter = Reflect.ownKeys(obj)[Symbol.iterator]()
return{[Symbol.iterator]() {
return this
},
next() {
let { done, value: key } = iter.next()
if (done) {
return { done: true}}return { value: [key, obj[key]] }
}
}
}
Copy the code
21.6.2 Iterative combinators
Combinators are functions that combine existing iterables to create new ones.
21.6.2.1 take(n, iterable)
Let’s take(n, iterable) from the combinatorial function, which returns the iterable of the first n terms.
function take(n, iterable) {
const iter = iterable[Symbol.iterator]()
return{[Symbol.iterator]() {
return this
},
next() {
if (n > 0) {
n--
return iter.next()
} else {
return { done: true }
}
}
}
}
const arr = ['a'.'b'.'c'.'d']
for (const x of take(2, arr)) {
console.log(x)
}
// Output:
// a
// b
Copy the code
This version of take() does not close the iterator iter. I’ll show you how to do this later, after I explain what closing iterators actually means.
21.6.2.2 zip(... iterables)
Zip converts n iterable entries into an n-tuple (encoded as an array of length N) of iterable entries.
function zip(. iterables) {
const iterators = iterables.map(i= > i[Symbol.iterator]())
let done = false
return{[Symbol.iterator]() {
return this
},
next() {
if(! done) {const items = iterators.map(i= > i.next())
done = items.some(item= > item.done)
if(! done) {return { value: items.map(i= > i.value) }
}
// Done for the first time: close all iterators
for (const iterator of iterators) {
if (typeof iterator.return === 'function') {
iterator.return()
}
}
}
// We are done
return { done: true}}}}Copy the code
As you can see, the shortest iterable determines the length of the result:
const zipped = zip(['a'.'b'.'c'], ['d'.'e'.'f'.'g'])
for (const x of zipped) {
console.log(x)
}
// Output:
// ['a', 'd']
// ['b', 'e']
// ['c', 'f']
Copy the code
21.6.3 Infinitely iterable
Some iterations may never be done.
function naturalNumbers() {
let n = 0
return{[Symbol.iterator]() {
return this
},
next() {
return { value: n++ }
}
}
}
Copy the code
For infinite iterations, you must not go through all of its entries. For example, by breaking open from a for-of loop
for (const x of naturalNumbers()) {
if (x > 2) break // Interrupt here
console.log(x)
}
Copy the code
Or just access the beginning of the infinitely iterable:
const [a, b, c] = naturalNumbers()
// a=0; b=1; c=2;
Copy the code
Or use a combinator. Take () is a possibility:
for (const x of take(3, naturalNumbers())) {
console.log(x)
}
// Output:
/ / 0
/ / 1
/ / 2
Copy the code
The “length” of the iterable returned by zip() is determined by its shortest input iteration. This means that zip() and naturalNumbers() give you ways to number iterators of arbitrary (finite) length:
const zipped = zip(['a'.'b'.'c'], naturalNumbers())
for (const x of zipped) {
console.log(x)
}
// Output:
// ['a', 0]
// ['b', 1]
// ['c', 2]
Copy the code
21.7 FAQ: Iterables and iterators
21.7.1 Isn’t the Iterative protocol slow?
You might worry that the iterative protocol is slow, because each call to next() creates a new object. However, memory management for small objects is fast in modern engines, and in the long run, the engine can optimize iteration so that intermediate objects do not need to be allocated. There is more information about a post on ES-Discuss.
21.7.2 Can I reuse the same object more than once?
In principle, there’s nothing to stop an iterator from reusing the same iteration result object multiple times — I hope most things work just fine. However, if the client caches the iteration results, there is a problem:
const iterationResults = []
const iterator = iterable[Symbol.iterator]()
let iterationResult
while(! (iterationResult = iterator.next()).done) { iterationResults.push(iterationResult) }Copy the code
If an iterator reuses its iterated result object, iterationResults will typically contain the same object more than once.
21.7.3 Why does ECMAScript 6 Not have iterable combinators?
You may be wondering why ECMAScript 6 doesn’t have iterable combinators, tools for handling iterations, or tools for creating iterations. That’s because the plan is two-step:
- Step 1: Standardize the iteration protocol.
- Step 2: Wait for the library according to the protocol.
Eventually, one such library or fragments from several libraries will be added to the JavaScript standard library.
If you want to see what such a library might look like, look at the standard Python module itertools.
21.7.4 Are iterables difficult to achieve?
Yes, iterations are hard to implement – if you implement them manually. The next chapter covers generators (among other things) that help with this task.
The iteration protocol contains the following interface (I omitted throw() in Iterator, which is only supported by yield* and is optional) :
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
return? (value? : any) : IteratorResult; } interface IteratorResult {value : any;
done : boolean;
}
Copy the code
The specification has a section on iterative protocols.
21.8.1 iteration
Next () the rules:
- As long as the iterator still has the value to generate
x
,next()
Returns the object{ value: x, done: false }
. - After iterating through the last value,
next()
You should always return an object with a property of true.
21.8.1.1 IteratorResult
The attribute of the iterator result need not be true or false; it is sufficient to be able to represent true or false. All built-in language mechanisms allow you to omit done: false.
21.8.1.2 Iterables that return a new iterator versus Iterables that always return the same iterator
Some iterables are required to generate a new iterator each time. For example, array:
function getIterator(可迭代) {
return iterable[Symbol.iterator]()
}
const iterable = ['a'.'b']
console.log(getIterator(iterable) === getIterator(iterable)) // false
Copy the code
The other iterations return the same iterator each time. For example, the generator object:
function* elements() {
yield 'a'
yield 'b'
}
const iterable = elements()
console.log(getIterator(iterable) === getIterator(iterable)) // true
Copy the code
When you iterate over the same iterator many times, it doesn’t matter if the iterable produces a new iterator. For example, with the following function:
function iterateTwice(可迭代) {
for (const x of iterable) {
console.log(x)
}
for (const x of iterable) {
console.log(x)
}
}
Copy the code
Using the new iterator, you can iterate over the same iterable multiple times:
iterateTwice(['a'.'b'])
// Output:
// a
// b
// a
// b
Copy the code
If the same iterator is returned each time, it cannot:
iterateTwice(elements())
// Output:
// a
// b
Copy the code
Note that each iterator in the library is also iterable. Its method [symbol.iterator]() returns this, which means it always returns the same iterator (itself).
21.8.2 Closing iterators
The iteration protocol distinguishes between two ways to complete iterators:
- Exhaustion: The normal way to complete an iterator is to retrieve all of its values. In other words, keep calling
next()
Until it returns a propertydone
fortrue
The object. - Closing: Through a call
return()
Tell the iterator that you are not going to call next() again.
Call return() rule:
return()
Is an optional method that is not available to all iterators. Iterators with it are said to be closable.- It should only be called if iterators are not exhausted
return()
. For example, as long as “suddenly” (before it’s donereturn()
.for-of
callreturn()
). The following operations will cause an abrupt exit:break
.continue
(Tag with outer block),return
,throw
.
Implement the return() rule:
- The method call
return(x)
Objects should normally be generated{ done: true, value: x }
, but if the result is not an object, the language mechanism simply throws an error (source in spec). - call
return()
Later,next()
The returned object should alsodone
.
The following code shows that a for-of loop calls return() if it aborts a done iterator before receiving it. That is, if you abort after the last value is received, return() is even called. This is subtle, and you must be careful when iterating manually or implementing iterators.
function createIterable() {
let done = false
const iterable = {
[Symbol.iterator]() {
return this
},
next() {
if(! done) { done =true
return { done: false.value: 'a'}}else {
return { done: true.value: undefined}}},return() {
console.log('return() was called! ')}}return iterable
}
for (const x of createIterable()) {
console.log(x)
// There is only one value in the iterable and
// we abort the loop after receiving it
break
}
// Output:
// a
// return() was called!
Copy the code
21.8.2.1 Iterators that can be closed
An iterator can be closed if it has a return() method. Not all iterators can be closed. For example, Array iterators are not:
> let iterable = ['a'.'b'.'c'];
> const iterator = iterable[Symbol.iterator]();
> 'return' in iterator
false
Copy the code
By default, Generator objects are closed. For example, returned by the following generator function:
function* elements() {
yield 'a'
yield 'b'
yield 'c'
}
Copy the code
If return() is called on the result of elements(), the iteration is complete:
> const iterator = elements();
> iterator.next()
{ value: 'a'.done: false }
> iterator.return()
{ value: undefined.done: true }
> iterator.next()
{ value: undefined.done: true }
Copy the code
If an iterator cannot be closed, it can continue iterating over it after A sudden exit from the for-of loop (such as the one in line A) :
function twoLoops(iterator) {
for (const x of iterator) {
console.log(x)
break // (A)
}
for (const x of iterator) {
console.log(x)
}
}
function getIterator(可迭代) {
return iterable[Symbol.iterator]()
}
twoLoops(getIterator(['a'.'b'.'c']))
// Output:
// a
// b
// c
Copy the code
Instead, elements() returns an iterator that can be closed, while the second loop inside twoLoops() has nothing iterable:
function* elements() {
yield 'a'
yield 'b'
yield 'c'
}
function twoLoops(iterator) {
for (const x of iterator) {
console.log(x)
break // (A)
}
for (const x of iterator) {
console.log(x)
}
}
twoLoops(elements())
// Output:
// a
Copy the code
21.8.2.2 Preventing iterators from being closed
The following classes are a common solution to prevent iterators from being closed. It does this by wrapping iterators and forwarding all method calls except return().
class PreventReturn {
constructor(iterator) {
this.iterator = iterator
}
/** Must also be iterable, so that for-of works */
[Symbol.iterator]() {
return this
}
next() {
return this.iterator.next()
}
return(value = undefined) {
return { done: false, value }
}
// Not relevant for iterators: `throw()`
}
Copy the code
If we use PreventReturn, the result of generator Elements () will not be closed after the first loop of twoLoops() exits abruptly.
function* elements() {
yield 'a'
yield 'b'
yield 'c'
}
function twoLoops(iterator) {
for (const x of iterator) {
console.log(x)
break // abrupt exit
}
for (const x of iterator) {
console.log(x)
}
}
twoLoops(elements())
// Output:
// a
twoLoops(new PreventReturn(elements()))
// Output:
// a
// b
// c
Copy the code
There is another way to make generators unshutable: all generator objects generated by the generator function elements() have the prototype object elements.prototype. With elements. Prototype, you can hide the default implementation of return(), which resides in the prototype of Elements. Prototype, as follows:
// Make generator object unclosable
// Warning: may not work in transpilers
elements.prototype.return = undefined
twoLoops(elements())
// Output:
// a
// b
// c
Copy the code
21.8.2.3 throughtry-finally
Clean up the generator
Some generators need to be cleaned up after the iteration (freeing allocated resources, closing open files, and so on). This is how we do it:
function* genFunc() {
yield 'a'
yield 'b'
console.log('Performing cleanup')}Copy the code
In a normal for-of loop, everything is fine:
for (const x of genFunc()) {
console.log(x)
}
// Output:
// a
// b
// Performing cleanup
Copy the code
However, if the loop exits after the first yield, the execution seems to stay there forever and never reaches the cleanup step:
for (const x of genFunc()) {
console.log(x)
break
}
// Output:
// a
Copy the code
What actually happens is that whenever you leave the for-of loop prematurely, for-of sends a return() to the current iterator. This means that the cleanup step is not completed because the generator function returns early.
Thankfully, this problem can be easily solved by performing cleanup in the finally clause:
function* genFunc() {
try {
yield 'a'
yield 'b'
} finally {
console.log('Performing cleanup')}}Copy the code
Now everything is working as expected:
for (const x of genFunc()) {
console.log(x)
break
}
// Output:
// a
// Performing cleanup
Copy the code
Thus, the general pattern for using resources that need to be shut down or cleaned up in some way is:
function* funcThatUsesResource() {
const resource = allocateResource();
try{...}finally{ resource.deallocate(); }}Copy the code
21.8.2.4 Handle cleanup in manually implemented iterators
const iterable = {
[Symbol.iterator]() {
function hasNextValue() {...}function getNextValue() {...}function cleanUp() {...}let returnedDoneResult = false;
return {
next() {
if (hasNextValue()) {
const value = getNextValue();
return { done: false.value: value };
} else {
if(! returnedDoneResult) {// Client receives first `done` iterator result
// => Won't call 'return()
cleanUp();
returnedDoneResult = true;
}
return { done: true.value: undefined}; }},return() { cleanUp(); }}; }}Copy the code
Note that the first time you return a done iterator result, you must call cleanUp(). You can’t do it ahead of time because return() might still be called. In order to do that, it can be tricky.
21.8.2.5 Close the iterator you use
If iterators are used, they should be closed properly. In generators, you can make for-of do all the work for you:
/** * Converts a (potentially infinite) sequence of * iterated values into a sequence of length `n` */
function* take(n, iterable) {
for (const x of iterable) {
if (n <= 0) {
break // closes iterable
}
n--
yield x
}
}
Copy the code
If you manage manually, you need to do a few things:
function* take(n, iterable) {
const iterator = iterable[Symbol.iterator]()
while (true) {
const { value, done } = iterator.next()
if (done) break // exhausted
if (n <= 0) {
// Abrupt exit
maybeCloseIterator(iterator)
break
}
yield value
n--
}
}
function maybeCloseIterator(iterator) {
if (typeof iterator.return === 'function') {
iterator.return()
}
}
Copy the code
If you don’t use generators, you need to do more:
function take(n, iterable) {
const iter = iterable[Symbol.iterator]()
return{[Symbol.iterator]() {
return this
},
next() {
if (n > 0) {
n--
return iter.next()
} else {
maybeCloseIterator(iter)
return { done: true}}},return() {
n = 0
maybeCloseIterator(iter)
}
}
}
Copy the code
21.8.3 listing
-
Record iterable: Provide the following information.
- Does it return a new iterator each time or the same iterator?
- Are its iterators closed?
-
Implement iterators:
- If the iterator runs out or is
return()
Call, the cleanup activity must take place.- In the generator,
try-finally
You can handle both cases in one place.
- In the generator,
- through
return()
After the iterator is closed, it should not passnext()
Generates any iterator results.
- If the iterator runs out or is
-
Manually using iterators (via for-of, etc.) :
- Don’t forget to pass
return
Close the iterator if and only if you haven’t exhausted it. This can be tricky.
- Don’t forget to pass
-
Continue iterating over iterators after a sudden exit: Iterators must be either unlockable or unlockable (for example, through utility classes).
The original address: www.kancloud.cn/chandler/ex…