Symbol.iterator makes anything work with for-of

for...of is not array syntax; it works on anything implementing the iterable protocol, which is one well-known method returning an object with a next().

class Paginator {
  constructor(fetchPage) { this.fetchPage = fetchPage; }

  *[Symbol.iterator]() {
    let page = 1, batch;
    while ((batch = this.fetchPage(page++)).length) {
      yield* batch;
    }
  }
}

for (const row of new Paginator(fetchPage)) { /* ... */ }

A generator method is the short way to implement it — *[Symbol.iterator]() — and the consumer never learns that pages exist. Spread and destructuring use the same protocol, so an iterable also works with [...paginator], which will happily exhaust an infinite sequence. Objects are not iterable by default, which is why for...of over a plain object throws while for...in does not.