DEV Community

Daniel Gropp
Daniel Gropp

Posted on

for ... of operator and Symbol.iterator

JavaScript's for ... of operator loops over iterable objects. This type of loop exists in many programming languages.

// JavaScript
for (const user of users) {
  /* do something */
}
Enter fullscreen mode Exit fullscreen mode
// Java
for (User user : users) {
  /* do something */
}
Enter fullscreen mode Exit fullscreen mode
// PHP
foreach ($users as $user) {
  /* do something */
}
Enter fullscreen mode Exit fullscreen mode
# Python
for user in users:
  # do something
Enter fullscreen mode Exit fullscreen mode

In JavaScript we also have the for ... in operator that loops over an object's enumerable properties, which means its keys.

const john = {name: "John Lennon", age: 40, isAlive: false};
const beatles = ["John", "Paul", "George", "Ringo"];

for (const key in john) {
  console.log(key); // "name", "age", "isAlive"
}
for (const key in beatles) {
  console.log(key); // 0, 1, 2, 3
}
Enter fullscreen mode Exit fullscreen mode

JavaScript arrays are basically special objects with indices as keys. We can imagine that a very simple implementation of a JavaScript array will look like this:

const arrayObject = {
  0: "John", 
  1:  "Paul", 
  2:  "George", 
  3: "Ringo", 
  length: 4
}
Enter fullscreen mode Exit fullscreen mode

that's why the for ... in operator will loop over its indices.
Using a for ... of operator on an array will loop over its entries.

for (const beatle of beatles) {
  console.log(beatle); // "John", "Paul", "George", "Ringo"
}
Enter fullscreen mode Exit fullscreen mode

But using the same operator on a plain object will throw an error.

for (const value of john) {
  console.log(value); // Uncaught TypeError: john is not iterable
}
Enter fullscreen mode Exit fullscreen mode

Our simple array implementation, will work using a regular for loop, but will throw the same error when using the for ... of operator.

for (let i = 0; i < arrayObject.length; i++) {
  console.log(arrayObject[i]); // "John", "Paul", "George", "Ringo"
}
for (const beatle of arrayObject) {
  console.log(beatle); 
  // Uncaught TypeError: arrayObject is not iterable
}
Enter fullscreen mode Exit fullscreen mode

JavaScript arrays (and for that matter also Set, Map, NodeList, etc.) are basically objects, So why on earth does a for ... of work on an array and not on plain objects?
The reason is a property called Symbol.iterator, which accepts a Generator function that allows for any object to be iterated with a for ... of loop and accept the spread syntax.
Array.prototype and other iterable interfaces, have that property defined,

Array.prototype[Symbol.iterator]; // ƒ values() { [native code] }
Set.prototype[Symbol.iterator]; // ƒ values() { [native code] }
Map.prototype[Symbol.iterator]; // ƒ entries() { [native code] }
NodeList.prototype[Symbol.iterator]; // ƒ values() { [native code] }
Enter fullscreen mode Exit fullscreen mode

while other objects don't.

Object.prototype[Symbol.iterator]; // undefined
Date.prototype[Symbol.iterator]; // undefined
Enter fullscreen mode Exit fullscreen mode

So, if for some reason we'd really like to use a for ... of loop on an object, we could define it a Symbol.iterator method. (Note that it's currently impossible to use arrow functions for a generator function).

john[Symbol.iterator] = function* () {
  for (const key in john) {
    yield john[key];
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we can use a for ... of loop on that object, and even use the spread syntax.

for (const value of john) {
  console.log(value); // "John Lennon", 40, false
}

const values = [...john]; // ["John Lennon", 40, false]
Enter fullscreen mode Exit fullscreen mode

But, setting a Symbol.iterator property to every object instance is overtly complicated and unnecessary. Instead, we can add it to Object.prototype.

Object.prototype[Symbol.iterator] = function* () {
  for (const key in this) {
    yield this[key];
  }
}
Enter fullscreen mode Exit fullscreen mode

And while this works like a charm, adding methods to built-in prototypes is discouraged. Think of the confusion it can cause with people unfamiliar with your code, and of future errors it could cause, read more about this here.
To iterate over the values of an object, we'd better use the Object.prototype static method Object.values, and then we could easily use a for ... of loop, or any Array.prototype methods.

for (const value of Object.values(john)) {
  console.log(value); // "John Lennon", 40, false
}
Enter fullscreen mode Exit fullscreen mode

So when should we define a Symbol.iterator method? For instance when we implement our own data structure, and we'd like it to be iterable. Check out my linked list implementation on GitHub

class LinkedList {

  /* Class implementation */

  [Symbol.iterator]() {
    return function* () {
      let node = this.head;
      while (node !== null) {
        yield node.value;
        node = node.next;
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Also, we can create a class that handle array-like objects, that will add the ability to use for ... of loops. That way we could use the object as is without using Object.prototype static methods and without polluting Object.prototype.

class IterableObject {
  constructor(obj) {
    Object.assign(this, obj);
  }

  * [Symbol.iterator]() {
    for (const key in this) {
      yield this[key];
    }
  }
}

const beatles = new IterableObject({
  john: {name: "John Lennon", age: 40, isAlive: false},
  paul: {name: "Paul McCartney", age: 79, isAlive: undefined},
  george: {name: "George Harrison", age: 58, isAlive: false},
  ringo: {name: "Ringo Starr", age: 81, isAlive: true}
});
Enter fullscreen mode Exit fullscreen mode

Now we can use a for ... of loop and run queries and tests on it using the spread syntax.

for (const beatle of beatles) {
  console.log(beatle.name);
  // "John Lennon", "Paul McCartney", "George Harrison", "Ringo Starr"
}

[...beatles].filter((beatle) => beatle.isAlive).length; // 1
Enter fullscreen mode Exit fullscreen mode

One thing to keep in mind about for ... of operator and Symbol.iterator, is that according to caniuse.com, while widely supported in 95% of browsers, they are not supported in (you guessed it) Internet Explorer. If you care about IE (and I think you shouldn't) you should avoid using this operator, as there's no polyfill - you can't polyfill syntax...

Top comments (1)

Collapse
 
lexlohr profile image
Alex Lohr

Other options include for-of loops to use the iterator protocol's semantic sugar or Object.keys/values/entries in combination with Array.prototype.forEach/map/reduce/etc, though the latter will create arrays in memory to iterate over, which can result in degraded performance.