There is a new syntax in ES6
for so-called generator-functions.
If there is an *
(asterisk) behind the keyword function
, it is a generator function.
The asterisk can be understood as multiplicity, meaning "several values".
It can be separated by spaces in any way,
like on the following generators *foo()
, *bar()
and *foobar()
:
function* foo() { yield "foo" } function *bar() { yield "bar" } function * foobar() { yield* foo() yield * bar() } const foobarArray = [ ...foobar() ] alert(foobarArray) // "foo,bar"
A generator function is not a normal function. If you call it, its body is not executed.
Instead, an instance
of the body is returned, wrapped into an
Iterator.
Calling the Iterator's next()
will iterate the body.
The body must use yield
instead of return
to output iteration values.
The wrapping Iterator will go from yield
to yield
, until all are done.
An optional *
(asterisk) behind the yield
again stands for multiplicity,
i.e. an Iterator is expected to the right of it.
Alternatively to calling next()
you can use a
for-of loop, apply the
spread-operator (like in the example above),
or make a destructuring-statement (see examples below).
On the web, you find the sentence
Generators are functions that can be paused and resumed
I think this needs a little more explanation.
The Iterator returned from a generator function can go into a
wait-for-input state through a yield
statement that has nothing on its right side.
A next(input)
call with a parameter would then move the Iterator to the next yield
,
whereby the input-parameter is put on the place where the yield
was.
Should the next yield
be an output with a non-empty right side,
it will also be performed by this next(input)
call.
Let's look at examples to find out what that all means.
ES6 Generator Function Tests
Click onto one of the buttons to get an example script into the text area. Below the script you find an explanation.
Resume
Generator functions come with a lot of abstract words like pausing and resuming, cooperative multitasking, coroutines etc.
Looking at the concrete behavior of generator functions may be easier than finding out all about these terms.
This is still work in progress. ES7 will introduce an await
keyword,
so asynchronous processing via generators may change in future.
A generator function does not execute its body when you call it.
Instead it returns an
instance
of its body, wrapped into an
Iterator.
Each call to the generator function creates a new Iterator instance.
Inside the body, the yield
statement must be used instead of return
.
Should there be a return, the iteration will terminate at that point.
Else it will go from one yield
to the next, until all are done, always outputting what is right of it.
The example uses a for-of loop to create the test-output.
This example introduces a generator function in a class, and thus the function is called method.
The function
keyword must be left out (else "SyntaxError: missing : after property id"),
so the asterisk looks a little lonely in front of the *keyValueTuples()
generator method.
The example class ObjectAnalyzer
yields all property / value tuples
of an object given in constructor, as an array of two.
The for-of loop receives that array in a [ key, value ]
destructuring.
When a generator function contains a yield
without argument,
this waits for input through a next(argument)
call.
That means, it puts whatever is passed as argument to next()
in place of the yield
.
This example defines a generator function that has three yields,
the first and the second being inputs, the last being an output.
First we must call an empty next()
to get to the first yield
of the "newborn" iterator.
We could call it the "start"-next, any parameter will be ignored.
The next("Hello")
then goes to the local constant first
, and
next("World")
to second
constant.
There, at the third call, also the output-yield
is done, and we receive "Hello World" from this call.
Try to replace the yield first+" "+second
by return first+" "+second
.
You would fail in the helloWorld.done === true
condition,
and you would have to change it to helloWorld.done === false
.
That means, a return triggers done === true
, but the last yield does not do such.
See also example "End of iteration return".
A yield
with a trailing *
(asterisk) expects an Iterable to the right of it.
That means, the generator function body will stop at the yield
statement,
output the first value of the Iterable to the right, on next call output the next value, and so on.
In the example, this is shown twice.
The arrayIterator()
generator uses the yield*
to output all elements of a given array parameter.
The delegationGenerator()
generator uses it to output all values yielded by arrayIterator()
.
A while-loop is used to create the test-output.
Of course this example is not really useful, but it shows that yield*
can go to any depth.
This example tries to make clear the role of a return
statement in a nested generator function.
The *ab()
generator yields "a" and "b", additionally it returns a string "End of iteration return".
An iterator is retrieved from *ab()
. When we would destructure or spread it,
or loop it in a for-of, we wouldn't get the "End of iteration return",
because these instructions ignore the last iteration element that has done === true
.
We can get the return just when looking at the last iteration element, by reading away all elements with
done === false
, and then retrieving the value from the last one.
Mind that a return
terminates the Iterator,
any yield
after it makes no sense and will be silently ignored.
This example summarizes the ways how the iterator from a generator function can be iterated. The most elegant way is most likely the for-of loop. The spread-operator does not provide terminating the iteration prematurely, it will always retrieve all elements. Destructuring may not always be the best solution, because it makes assumptions about the return that may not hold. The longest one surely is the while-next loop, but it is the only one that is able to also receive an optional return value.
Keine Kommentare:
Kommentar veröffentlichen