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)