Iterable protocol

The iterable protocol allows values containing sequences to share a common set of characteristics, allowing them to all be iterated over or treated in a similar way. 

We can say that an object that implements the iterable protocol is iterable. Iterable objects within JavaScript include ArrayMapSet, and String

Any object can define its own iterable protocol by simply supplying an iterator function under the property name's Symbol.iterator (which maps to the internal @@iterator property).

This iterator function must fulfill the iterator protocol by returning an object with a next function. This next function, when called, must return an object with done and value keys indicating what the current value of the iteration is and whether the iteration is completed:

const validIteratorFunction = () => {
return {
next: () => {
return {
value: null, // Current value of the iteration
done: true // Whether the iteration is completed
};
}
}
};

So, to be utterly clear about this, there are two distinct protocols:

  • The iterable protocol: Any object that implements an @@iterator via [Symbol.iterator] fulfills this protocol. Native examples include Array, String, Set, and Map.
  • The iterator protocol: Any function that returns an object of the form {... next: Function} and whose next method, when called, returns an object in the following form: {value: Boolean, done: ...}.

For an object to fulfill the iterable protocol, it must implement [Symbol.iterator], like so:

const zeroToTen = {};
zeroToTen[Symbol.iterator] = function() {
let current = 0;
return {
next: function() {
if (current > 10) return { done: true };
return {
done: false,
value: current++
};
}
}
};

// We can see the effect of the iterable via the spread operator:
[...zeroToTen]; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Providing custom methods of iteration via the iterable protocol can be useful when you want to control the order of iteration or if you want to somehow process, filter, or generate values during iteration. Here, for example, we are specifying an iterator function as a generator function, which, as you may recall, returns a generator that fulfills both the iterator and iterable protocols. This generator function will yield two variants for every word stored – one uppercase and one lowercase:

const words = {
values: ['CoFfee', 'ApPLE', 'Tea'],
[Symbol.iterator]: function*() {
for (let word of this.values) {
yield word.toUpperCase();
yield word.toLowerCase();
}
}
};

[...words]
// => ["COFFEE", "coffee", "APPLE", "apple", "TEA", "tea"]

Specifying iterator functions as generator functions like this is far simpler than having to manually implement the iterator protocol. Generators naturally fulfill this contract, so they can be used far more seamlessly. Generators also tend to be more readable and succinct and have the dual benefit of implementing both the iterator and iterable protocols, meaning that they can be used to decorate an object with iteration capabilities:

const someObject = {
[Symbol.iterator]: function*() { yield 123; }
};

[...someObject]; // => [123]

They can, themselves, also provide that iteration capability:

function* someGenerator() {
yield 123;
}

[...someGenerator()]; // => [123]

It's important to keep in mind that any work that's done within a custom iterable should be in line with the expectations of consumers. Iteration is usually considered a read-only operation, so you should steer clear of mutations of the underlying value-set during iteration. Implementing your own iterables can be incredibly powerful, but can also lead to unexpected behavior by the consumers of your code who aren't aware of your custom iteration logic.

It's vital to balance the convenience of custom iteration for those people who are in the know with those people who might only be experiencing your interface or abstraction for the first time.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset