Imagine you are building an abstraction for a classroom, and one responsibility of a classroom object is to encapsulate the names of the students in the classroom. You might build a class like the following.
class Classroom { constructor() { this.students = ["Tim", "Joy", "Sue"]; } }
On occasion, the outside world needs to know the names of the students inside the classroom. Perhaps the names will be shown on the screen, or saved in a database. In either case, how should a classroom object give the student names to a caller? One option is to return a reference to the students array, in which case the caller might change the array by adding or removing items. This option isn’t the best option if the classroom want to protect the array and validate the addition or removal of a student.
Another option is to make a copy of the array. Then, even if someone changes the copy, the original student array remains safe. However, copy operations can be expensive.
With ES6 and iterators we now have the ability to provide access to a collection without giving someone access to a mutable array. All we need is to make the classroom iterable by adding a Symbol.iterator method.
First, let’s make the classroom iterable by building our own iterator objects. This is the hard way to solve the problem.
class Classroom { constructor() { this.students = ["Tim", "Joy", "Sue"]; } [Symbol.iterator]() { var index = 0; return { next: () => { if(index < this.students.length){ index += 1; return {done: false, value: this.students[index-1]}; } return { value: undefined, done: true }; } } } }
The above code adds the iterator method to the class. Notice how Symbol.iterator needs square brackets to surround the method “name”. Inside the method is the code to return objects with a next method, which in turn returns objects with done and value properties. A classroom should now behave just like an array and allow developers to use for of loops.
let scienceClass = new Classroom(); let result = []; for(let name of scienceClass) { result.push(name); } expect(result).toEqual(["Tim", "Joy", "Sue"]);
One might notice that the code inside the classroom iterator method is the type of code that works with any array. Let’s extract the code into a new class.
class ArrayIterator { constructor(array) { this.array = array; this.index = 0; } next() { let result = { value: undefined, done: true}; if(this.index < this.array.length) { result.value = this.array[this.index]; result.done = false; this.index += 1; } return result; } }
Now we can reuse ArrayIterator from several places in an application.
class Classroom { constructor() { this.students = ["Tim", "Joy", "Sue"]; } [Symbol.iterator]() { return new ArrayIterator(this.students); } }
Even though the ArrayIterator class is helpful, we will see an even easier implementation of the iterator method when we get to generators.