The following example defines a custom data structure, Range, which allows iteration. The simplest way to make an object iterable is to provide an [@@iterator]() method in the form of a generator function:
class Range {
#start;
#end;
#step;
constructor(start, end, step = 1) {
this.#start = start;
this.#end = end;
this.#step = step;
}
*[Symbol.iterator]() {
for (let value = this.#start; value <= this.#end; value += this.#step) {
yield value;
}
}
}
const range = new Range(1, 5);
for (const num of range) {
console.log(num);
}
This works, but it isn't as nice as how built-in iterators work. There are two problems:
- The returned iterator inherits from
Generator, which means modifications to Generator.prototype are going to affect the returned iterator, which is a leak of abstraction.
- The returned iterator does not inherit from a custom prototype, which makes it harder if we intend to add extra methods to the iterator.
We can mimic the implementation of built-in iterators, such as map iterators, by subclassing Iterator. This enables us to define extra properties, such as @@toStringTag, while making the iterator helper methods available on the returned iterator.
class Range {
#start;
#end;
#step;
constructor(start, end, step = 1) {
this.#start = start;
this.#end = end;
this.#step = step;
}
static #RangeIterator = class extends Iterator {
#cur;
#s;
#e;
constructor(range) {
super();
this.#cur = range.#start;
this.#s = range.#step;
this.#e = range.#end;
}
static {
Object.defineProperty(this.prototype, Symbol.toStringTag, {
value: "Range Iterator",
configurable: true,
enumerable: false,
writable: false,
});
delete this.prototype.constructor;
}
next() {
if (this.#cur > this.#e) {
return { value: undefined, done: true };
}
const res = { value: this.#cur, done: false };
this.#cur += this.#s;
return res;
}
};
[Symbol.iterator]() {
return new Range.#RangeIterator(this);
}
}
const range = new Range(1, 5);
for (const num of range) {
console.log(num);
}
The subclassing pattern is useful if you want to create many custom iterators. If you have an existing iterable or iterator object which doesn't inherit from Iterator, and you just want to call iterator helper methods on it, you can use Iterator.from() to create a one-time Iterator instance.