Iterate over data structures is like Programming 101
.
This just works:
for (const item of [1,2,3]) {
console.log(item)
}
// 1
// 2
// 3
or using a Map
const map = new Map()
m.set('a', 'first value')
m.set('b', 'second value')
for (const [key, value] of m) {
console.log(key + " » " + value)
}
// a » first value
// b » second value
But, not saying anything new, Objects are not iterable
const obj = { a: 'item a', b: 'item b' }
for (const [key, value] of obj) {
console.log(key + " » " + value)
}
// ⚠️ Uncaught TypeError: obj is not iterable
why? 🤔
Array
, Map
, Set
... built-in objects implement the iterable protocol while Object
doesn't.
Summarizing official docs, any object (or one of the objects up its prototype chain) which defines the @@iterator
method is iterable.
@@iterator
method is just notation. A convention for naming "a method that is accessible through Symbol.iterator
symbol that [[fulfills the Iteration protocol]]"
... wait what? 🙃
This notation: Foo[@@someKey]
is naming the property of Foo
accessible through Symbol.someKey
; easier to understand with code:
{
...
[Symbol.someKey]: /* we're naming this property */
}
Really I haven't found any official mention in the docs explaining this notation but inferred from usage across the docs.
When the docs refers to @@iterator
:
{
...
[Symbol.iterator]: () => {
/* we need to implement this method */
}
}
Now, js needs this method to... return an object with a method named next() that returns bla bla bla; something like this:
{
...
[Symbol.iterator]: () => {
return {
next() {
...
return { done: true, value: i };
},
}
}
}
OR (and this is really cool) this function can be a Generator function
from the docs:
The Generator object is returned by a generator function and it conforms to both the iterable protocol and the iterator protocol.
{
...
*[Symbol.iterator]() {
/* a generator function here does the trick */
yield i
}
}
To Be Honest: I have never found the chance to use generator functions in real life... they are just one more tool in my toolbox 🧰
Assemble! 🦸🏾♂️
An object that defines (in @@iterator
) a function that yields each [key, value]
, is iterable:
const obj = {
a: 'first value',
b: 'second value',
*[Symbol.iterator] () {
for (const k in this) {
yield [k, this[k]]
}
},
}
for (const [key, value] of obj) {
console.log(key + " : " + value)
}
// a : first value
// b : second value
Since it's iterable, it may also use destructuring:
const obj = {
a: 3,
b: 100,
*[Symbol.iterator] () {
for (const k in this) {
yield [k, this[k]]
}
},
}
[...obj].find(([_, value]) => value > 99 )
// Array [ "b", 100 ]
I've uploaded to npm
a tiny utility package that does this for you in case you are curious:
import { iterable } from "@raw-js/iterable";
const obj = { ... }
/* for..of */
for (const [key, value] of iterable(obj)) {
console.log(key + ": " + value);
}
/* destructuring */
[...iterable(obj)]
Final thoughts 🧳
Do I include this method on the prototype chain of my objects, in order to make them iterable?
NO.
I find this an interesting exercise for deep-understanding js. But that's it. This is a "how does this work" exercise.
In real life, js does implement this idea thanks to Object.entries()
for (const [key, value] of iterable(obj)) {}
===>
for (const [key, value] of Object.entries(obj)) {}
Implementing @raw-js/iterable
helped me understanding more about Symbols, Iterators, Generators, etc. But in my day to day work I'd prefer Object.entries()
Banner image from Storyset
Thanks for reading 💚.
Top comments (0)