DEV Community

Alex MacArthur
Alex MacArthur

Posted on • Originally published at macarthur.me on

More Elegant Destructuring with JavaScript Generators

JavaScript has a standard for defining an object’s iteration behavior. In fact, it’s at work whenever you destructure an array or use it in a for..of loop.

const arr = ['first', 'second', 'third'];

// Using destructuring: 
const [first, second, third] = arr;

// Using a `for..of` loop: 
for(const item of arr) {
    doStuff(item);
}

Enter fullscreen mode Exit fullscreen mode

But it’s not just for built-in objects. You can make any object iterable by giving it a Symbol.iterator function property that returns an object adhering to the iterator protocol. In its most basic form, that returned object has next() method returning another object with value and done properties.

For example, here’s an iterable that provides a range of numbers (1 through 3):

let count = 1;

const iterableObj = {
  [Symbol.iterator]() {
    return {
      next() {
        return {
          done: count === 4,
          value: count++,
        };
      },
    };
  },
};

for (const item of iterableObj) {
  console.log(item);
}

// 1
// 2
// 3

Enter fullscreen mode Exit fullscreen mode

Correctly setting that done property is critical, because that’s how the for loop will know when to discontinue execution, or, if you’re destructuring, when you’ll start getting undefined values:

const [a, b, c, d] = iterableObj;

// 1, 2, 3, undefined

Enter fullscreen mode Exit fullscreen mode

Interesting, but… practical?

While all that’s cool, there’s a notorious lack of “real” use cases for custom iterables in the wild. At least, that’s the story as I’ve experienced it.

And then I saw this tweet from Alex Reardon. In his example, an iterator and destructuring are used to generate an arbitrary number of objects on the fly (DOM elements, in his case). Here’s that implementation (a smidge modified, but the same spirit):

function getElements(tagName = 'div') {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return {
            done: false,
            value: document.createElement(tagName),
          };
        },
      };
    },
  };
}

const [el1, el2, el3] = getElements('div');

console.log(el1, el2, el3); 
// HTMLDivElement, HTMLDivElement, HTMLDivElement

Enter fullscreen mode Exit fullscreen mode

At first, it looked unnecessarily complicated, especially when considering what I’d personally otherwise do with an array:

function getElements(tagName = 'div', number) {
  return new Array(number).fill(document.createElement(tagName));
}

const [el1, el2, el3] = getElements('div', 3);

console.log(el1, el2, el3);
// HTMLDivElement, HTMLDivElement, HTMLDivElement

Enter fullscreen mode Exit fullscreen mode

Despite needing to specify how many items you’d like to generate up front, this sort of approach had always satisfied me. But then I remembered that there’s more than one way to make your own iterable in JavaScript.

Generators: Write Simpler Iterables

If using Symbol.iterator is too verbose, you can syntactically sweeten it with a generator, a special function returning a Generator object that functions the same way as a hand-written iterable. Here’s that same method written as a generator function:

function* getElements(tagName = 'div') {
  while (true) {
    yield document.createElement(tagName);
  }
}

const [el1, el2, el3] = getElements('div', 3);

console.log(el1, el2, el3);

Enter fullscreen mode Exit fullscreen mode

That infinite while loop may be startling, but it’s safe — the yield keyword won’t allow it to continue to run until the generator is invoked each time, whether it be via for..of (which you can even use asynchronously), destructuring, or something else.

Some Perks to Destructuring with a Generator

This change made it much more appealing to use iterables in destructuring items, for a few reasons:

  1. I don’t need to specify how many items I want to generate up front. As such, the function’s signature is a little simpler (the number parameter is no longer required).
  2. There’s a (small) performance advantage. Items are produced on demand, not up front. I’ll never accidentally generate five items, end up only needing three, and leaving two orphans.
  3. In my opinion, it’s _ more _ elegant than filling an array. You can even get the function body down to a single line:
function* getElements(tagName = 'div') {
  while (true) yield document.createElement(tagName);
}

Enter fullscreen mode Exit fullscreen mode

And in addition to those benefits, an honorable mention: the increased social credibility that comes with telling people you’ve had a reason to use a generator.

Time Will Yield More

When you’ve used a certain set of tools for countless jobs, it’s not easy to be given a new tool and expect to immediately know how it’s uniquely suited to solve certain problems. So, despite this being one of the first “practical” use cases I’ve for a generator, I don’t expect it to be the last. After all, it’s still a new tool for me.

That said, I’m eager to hear about other reasons iterators and generators are useful for a job, no matter how insignificant. If you have ‘em, yield ’em!

Top comments (0)