Transcript from the "Generators" Lesson
>> Kyle Simpson: Even though it's not that difficult to make an object with a next method on it, and define what it does, I think we can all agree that this is a fairly imperative approach to adopting the iterator protocol for an object. So it would be nice if there was some other way of more declaratively creating an iterator producing function.
[00:00:22] And that leads us to generators. On line 1 you see an * appear at the very top. Right at the very top you see an * that indicates that we are dealing with a special kind of function called a generator. This is a new type of function that was added in the ES6, and there's lots of complexities to generators that we're not gonna cover now.
[00:00:49] But the one thing that you need to understand is that generators when you invoke them, they don't run, they produce an iterator like we see here on line 8. And that iterator, since it's a standard iterator, has a dot next method on it like we see on line 10.
[00:01:09] And when you call dot next on a iterator that's attached to a generator, it is going to give you the value that was yielded out from that generator. This new keyword yield allows a generator to produce additional values every time it's iterated over. So when I say yield 1, we get out and iterate a result that says value 1 done false.
[00:01:35] And when I say yield 2 we get value to done false and then value 3 done false. And then you'll notice because I use a return keyword on line 5 when we get the value 4 out, now it says done true, because we know for sure that iterator doesn't have any other results to produce.
[00:01:58] There is, however, a bit of a gotcha with the usage of the return keyword inside a generator. Look down on line 15, when I do a dot-dot-dot over this generator, and it consumes the iterator according to that protocol, as soon as it sees a done true, it stops.
[00:02:18] It doesn't keep the value at that moment it assumes that it is iterated past it, so that is the effect of throwing away the value 4. It's why it doesn't show up in the array down online 15. So what you need to know is if you're gonna make your own iterator using a generator like this you wanna make sure that you always yield values instead of returning values.
[00:02:42] If you yield those values, they will come out according to the iterator protocol and then they'll be iterated over. It's generally considered a bad practice to return a value from a generator.
>> Kyle Simpson: So now, armed with this information, if we know that making a generator and yielding values that seems like a really good way of defining our own iterators and any object that we want.
[00:03:11] And if I could do a yield 1, 2, and 3, then I could also imagine, for example, having a for loop. And having a yield keyword inside of a for loop, and then yielding out values for each iteration of my loop. And that's how we'll actually define our iterator for our obj object.
[00:03:31] Line 5, you'll notice that I have this star here that indicates that this function is going to be that generator type. I'm putting it at that special location symbol.iterator, and it's just a concise method it doesn't take any inputs here. Its behavior by default is to iterate over all the keys.
[00:03:51] And by the way, I'm using a for-of loop inside of my iterator. Why does that work? Because object.keys returns me an array, and arrays are iterables. So here I'm gonna loop over all of the elements in the array, being keys, and then I'm simply going to yield out this [key], which would be the value, so it would be the value 1 and then the value 2 and the value 3.
[00:04:20] And the generator takes care of all of that plumbing about making iterators that have next methods that can be called, and preparing the iterator results, and all of that and all I need to focus on in this code is what value to yield out. That's what makes this a more declarative approach, is that I simply yield out my values.
[00:04:42] Here, I decided to make my iterator, in other words, the iteration over obj is that you don't care about the keys, you only care about the values. But imagine what would happen if instead of saying yield this of key, what would happen if I said yield key?
>> Kyle Simpson: Well, now the iteration would be seeing quote a, quote b, and quote c.
[00:05:04] So there are multiple kinds of iterations that might be useful. Iterating over an object's values would be useful. Iterating over its keys would be useful. And by the way it turns out we have object.keys and we have object.values and they produce an iteration over the keys and the values.
[00:05:27] But what about something that combine both of them? What if I wanted to get both the key and the value? Well then, I could define something called entries. An entry is a tuple, and a tuple is just a two-element array, and that's a fancy way of saying a two-element array.
[00:05:48] A tuple where the first element in the tuple is the key, and the second element is the value. So what would I do on line 7? I would return square bracket array and I'd return key comma, this of key. And then someone would be able to receive both the keys and the values as they iterated over my data structure.
>> Kyle Simpson: So we have object.values to iterate over an object's values that you don't have to define your own anymore, you can just use that built-in utility. We have object.keys, which actually, we've had since ES5, and that gives us an array of all the keys. And now we have object.entries, which gives us an array of all those tuples.
[00:06:38] So if that's the built-in way of doing that then all you have to do is on your data structures define those three iterators yourself. Iterate over your values, iterate over your keys, and iterate over your entries. You don't have to but it's a good idea to make it as easy for someone to consume your data structure as possible.
>> Kyle Simpson: If I then wanted to do something like dot dot dot, and not get the default, because if you do dot dot dot over an array, the default is to essentially give you array values. But If I wanted to be able to do dot dot dot and get an iteration over its indexes, then instead of saying dot dot dot array, I could say dot dot dot array dot keys.
[00:07:28] Or I could say dot dot dot array dot values or dot dot dot array dot entries, or a for-of loop over the dot values, dot entries or dot keys. All of those would be standard ways. So when you make your own data structures, it's a good idea not only to define your default iterator, like we have here on line 5, you're making a default iterator.
[00:07:51] But also go ahead expose any other way that would be useful for somebody to iterate over your data structure.
>> Student: I have a question.
>> Kyle Simpson: Yeah.
>> Student: Can you mutate your iterable in a for-of loop or are you gonna find yourself in a world of pain, or be prevented from that?
[00:08:12] Like if I wanted to remove something in the middle and I want to do it in a for-of loop how much is that gonna hurt?
[00:08:37] So you're not seeing that implementation detail here where I would've captured the list of keys at the beginning and then captured all its values and then looped them out. The built in ones behave as if that is the case. And here if you were to mutate it you would actually be affecting the iteration as it went.
[00:08:56] So you can do that but it can create some very weird results. Not for you as much as for the consumer of your system where they iterate something out, and they are like wait what happened to this value, or why did I get this value? So it's a good idea to consider not doing mutation in line with your iteration.
>> Student: I ask cuz some languages will prevent you from it, so [LAUGH] yeah.
[00:09:31] Don't make mutations in line with it.
>> Student2: Could you abstract that generator out of the object and refer to it in the object in that last space of the object?
>> Student: Something going on the generate side. [LAUGH]
>> Kyle Simpson: So I think that question is saying, do I have to list the generator as an in line concise method in the object literal, or can that be defined somewhere else?
[00:10:01] And the answer you can point a property, like symbol.iterator, at an existing function reference that exists externally to the object. It is much more common to define the iterator directly on the object. But if that doesn't make sense in your scenario, or maybe you're gonna put that iterator on the prototype of some object in your object chain or something like that.
[00:10:27] It doesn't physically have to actually be added to the object literal, just has to be accessible to that object when somebody tries to iterate over it. Because the dot-dot-dot operator and the for-of operator, they're sort of dumb, they're basically just gonna say, do you have access to a, can you give me something at this property location?
[00:10:46] And if at that moment they do, great, and if not, then they're not gonna be able to iterate over your object.
>> Kyle Simpson: Other questions about iterators and generators?