For loop problem
Before I start explaining what is the problem with the for loop and why it is worth using the for-of loop, let's take a look at the code below:
//I want to list all combinations of these three arrays
let colors = ["Red ", "Blue ", "Yellow "];
let cars = ["BMW ", "Audi ", "Fiat "];
let models = ["v1.", "v2.", "v3."];
let allOptionsForLoop = [];
let allOptionsForOfLoop = [];
//Let's use for loop to create allOptionsForLoop:
for(let i=0; i<colors.length; i++) {
for(let j=0; j<cars.length; j++) {
for(let k=0; k<models.length; k++) {
allOptionsForLoop.push(colors[i]+cars[j]+models[k]);
}
}
}
// And for-of loop to create allOptionsForOfLoop;
for(const color of colors) {
for(const car of cars) {
for(const model of models) {
allOptionsForOfLoop.push(color+car+model)
}
}
}
console.log(allOptionsForLoop)
console.log("_________________________")
console.log(allOptionsForOfLoop)
console.log(JSON.stringify(allOptionsForLoop)===JSON.stringify(allOptionsForOfLoop))
// [
'Red BMW v1.', 'Red BMW v2.',
'Red BMW v3.', 'Red Audi v1.',
'Red Audi v2.', 'Red Audi v3.',
'Red Fiat v1.', 'Red Fiat v2.',
'Red Fiat v3.', 'Blue BMW v1.',
'Blue BMW v2.', 'Blue BMW v3.',
'Blue Audi v1.', 'Blue Audi v2.',
'Blue Audi v3.', 'Blue Fiat v1.',
'Blue Fiat v2.', 'Blue Fiat v3.',
'Yellow BMW v1.', 'Yellow BMW v2.',
'Yellow BMW v3.', 'Yellow Audi v1.',
'Yellow Audi v2.', 'Yellow Audi v3.',
'Yellow Fiat v1.', 'Yellow Fiat v2.',
'Yellow Fiat v3.'
]
_________________________
[
'Red BMW v1.', 'Red BMW v2.',
'Red BMW v3.', 'Red Audi v1.',
'Red Audi v2.', 'Red Audi v3.',
'Red Fiat v1.', 'Red Fiat v2.',
'Red Fiat v3.', 'Blue BMW v1.',
'Blue BMW v2.', 'Blue BMW v3.',
'Blue Audi v1.', 'Blue Audi v2.',
'Blue Audi v3.', 'Blue Fiat v1.',
'Blue Fiat v2.', 'Blue Fiat v3.',
'Yellow BMW v1.', 'Yellow BMW v2.',
'Yellow BMW v3.', 'Yellow Audi v1.',
'Yellow Audi v2.', 'Yellow Audi v3.',
'Yellow Fiat v1.', 'Yellow Fiat v2.',
'Yellow Fiat v3.'
]
true
Do you see the differences?
The for-of loop allows you to completely eliminate the need to monitor the collection index, so you can concentrate on working with the collection's content.
But how does the for-of loop know how to iterate through a given collection?
ITERATORS
Before you fully understand how the for-of loop works, you need to understand the iterators. What are iterators?
An iterator is such an object with a rather unusual interface that is designed to iterate. The iterator contains an internal pointer that tracks our position in the collection, and a next()
method that contains the result object. The result object has two values {value, done}
, value contains the return value of the iterator, and done tells us whether the given value is the last value returned true
or not false
.
Based on this information, let's try to create such an iterator ourselves:
function myFirstIterator(itemsToIterate) {
// i is an "iteration pointer"
let i = 0;
return {
next: function() {
//check if itemsToIterate[i] exists
let done = i >= itemsToIterate.length;
//if its done return undefined
let value = !done ? itemsToIterate[i] : undefined;
//ok we've got {done, value} so lets move forward
i++;
return {
done,
value
}
}
}
}
let iterateMe = myFirstIterator(["Hello", "World", "!"]);
console.log("1. ", iterateMe.next()) // 1. { done: false, value: 'Hello' }
console.log("2. ", iterateMe.next()) // 2. { done: false, value: 'World' }
console.log("3. ", iterateMe.next()) // 3. { done: false, value: '!' }
console.log("4. ", iterateMe.next()) // 4. { done: true, value: undefined }
console.log("5. ", iterateMe.next()) // 5. { done: true, value: undefined }
As you can see, creating iterators that behave according to the rules described above (those defined in ES6) is not an easy task. Fortunately, ES6 comes to the rescue and offers us generators that make iterator creation easier. How?
GENERATORS
In simple words: generator is a function that returns an iterator. Let's look at the code below:
// * placed in front of myFisrstGenerator makes this function a generator
function *myFirstGenerator() {
//yield specifies the values that should be returned by the iterator when calling the next () method
yield "Hello";
yield "World";
yield "!"
}
let iterateMe = myFirstGenerator()
console.log("1. ", iterateMe.next()) // 1. { done: false, value: 'Hello' }
console.log("2. ", iterateMe.next()) // 2. { done: false, value: 'World' }
console.log("3. ", iterateMe.next()) // 3. { done: false, value: '!' }
console.log("4. ", iterateMe.next()) // 4. { done: true, value: undefined }
console.log("5. ", iterateMe.next()) // 5. { done: true, value: undefined }
After each yield command, the generator stops running the generator functions. This allows us to develop a generator that adds elements to iterators:
function *iterateMyArray(myArray) {
for(let i=0; i<myArray.length; i++) {
yield myArray[i]
}
}
let iterateMe = iterateMyArray(["Hello", "World", "!"])
console.log("1. ", iterateMe.next()) // 1. { done: false, value: 'Hello' }
console.log("2. ", iterateMe.next()) // 2. { done: false, value: 'World' }
console.log("3. ", iterateMe.next()) // 3. { done: false, value: '!' }
console.log("4. ", iterateMe.next()) // 4. { done: true, value: undefined }
console.log("5. ", iterateMe.next()) // 5. { done: true, value: undefined }
Now compare the generator * iterateMyArray()
with the iterator from the beginning. The difference in the ease of writing ES6 iterators with generators compared to ES5 is huge.
Generators tips and tricks
Creating a generator:
//✅✅✅
function* sampleGenerator() {}
//✅✅✅
function * sampleGenerator() {}
//✅✅✅
function *sampleGenerator() {}
//✅✅✅
function*sampleGenerator() {}
//✅✅✅
let sampleGenerator = function *() {}
//❌ ❌ ❌
let sampleGenerator = *() => {}
Be careful where you use yield:
//yield can be used only inside generators
//❌ ❌ ❌
function *newGen() {
function insideNewGen() {
//it will throw an error
yield "Hello"
}
}
You can add generators as an object method:
let obj = {
//like this (ES5)✅✅✅:
generatorNr1: function*(myArray) {
for(let i=0; i<myArray.length; i++) {
yield myArray[i]
}
},
//or like this (ES6)✅✅✅:
*generatorNr2(myArray) {
for(let i=0; i<myArray.length; i++) {
yield myArray[i]
}
}
}
[Symbol.iterator]
But what about that for loop? So much code for iterators and generators here, but how does that relate to the for loop? Good question. The iterator is associated with the element that iterates, that is the Symbol.iterator object. It specifies the function that returns the iterator for the given object. All iterable elements have a default Symbol.iterator defined. Thanks to this, they can use the for of loop because it is from such iterator the loop takes knowledge about its iteration. But do all elements have such a default iterator? Let's check:
function doIhaveDefaultIterator(obj) {
return typeof obj[Symbol.iterator] === 'function'
}
//array✅
console.log(doIhaveDefaultIterator(["Hello", "World", "1"])) //true
//Map✅
console.log(doIhaveDefaultIterator(new Map())) //true
//String✅
console.log(doIhaveDefaultIterator("hello")) //true
//Object❌
console.log(doIhaveDefaultIterator(new Object())) //false
console.log(doIhaveDefaultIterator({})) //false
//Set✅
console.log(doIhaveDefaultIterator(new Set())) //true
//Weakmap❌
console.log(doIhaveDefaultIterator(new WeakMap)) //false
//WeakSet❌
console.log(doIhaveDefaultIterator(new WeakSet)) //false
Wow. As you can see, not everything in JS can be iterated by default, it is especially painful that objects do not have an iterator. Does this mean that the for-of loop is useless for objects? Not necessarily ...
How to create an iterable object?
You need to create a new iterator using [Symbol.iterator]
let object = {
"name":"Michal",
"surname":"Zarzycki",
*[Symbol.iterator]() {
let keys = Object.keys(object);
for(let i=0; i<keys.length; i++) {
yield [`${keys[i]}: ${object[keys[i]]}`]
}
}
}
for(const props of object) {
console.log(props)
}
//[ 'name: Michal' ]
//[ 'surname: Zarzycki' ]
Looking at the example above, an observant might say that since this is just printing key-value pairs from an object, wouldn't it be better to just use the for ... in loop, which does just that? This is another good question. Although creating iterators or overwriting default iterators works in a bit more specific situations, where our iteration does not necessarily have to be typical, adding an iterator to the object may be useful for another reason: for..in does not see Symbols:
let symbol1 = Symbol('symbol1')
obj = {
"name":"Michal",
"surname":"Zarzycki",
}
obj[symbol1] = "Secret message: I love puppies"
for(const prop in obj) {
console.log(`${key}: ${prop[key]}`)
}
//console.log
//name: Michal
//surname: Zarzycki
Ufff, my secret message is safe now. Unless I use an iterator:
let symbol1 = Symbol('symbol1')
obj = {
"name":"Michal",
"surname":"Zarzycki",
*[Symbol.iterator]() {
let arr = Object.getOwnPropertySymbols(obj)
let ob_keys = Object.keys(obj);
let keys = [...arr, ...ob_keys]
for(let i=0; i<keys.length; i++) {
yield [keys[i], obj[keys[i]]]
}
}
}
obj[symbol1] = "Secret message: I love puppies"
for(const prop of obj) {
console.log(prop)
}
//console.log():
//[ Symbol(Symbol.iterator), [GeneratorFunction:[Symbol.iterator]] ]
//[ Symbol(symbol1), 'Secret message: I love puppies' ]
//[ 'name', 'Michal' ]
//[ 'surname', 'Zarzycki' ]
As you can see, iterators are a very powerful tool and they have many uses. I recommend you to create your own iterator, maybe it will skip your message secret when iterating 😃.
Thanks for reading! 🚀🚀🚀
Top comments (0)