Give a class a [Symbol.iterator]() method and every consumer accepts it. Writing the method as a generator (*[Symbol.iterator]()) is shortest: the engine builds next() for you, plus a return() that runs your finally block when a loop exits early, the place to release file handles or subscriptions.
class Playlist {
constructor(...songs) { this.songs = songs; }
*[Symbol.iterator]() { // the generator supplies next() and return()
try { yield* this.songs; } finally { console.log('closed'); }
}
}
const list = new Playlist('Intro', 'Verse', 'Chorus');
console.log([...list], Array.from(list, s => s.length));
for (const song of list) if (song === 'Verse') break; // break calls return()closed closed [ 'Intro', 'Verse', 'Chorus' ] [ 5, 5, 6 ] closed
Keep iteration state inside the iterator, never on the collection: an index stored in this is not reset, so a second loop would yield nothing.