Categories
Modern JavaScript

Best of Modern JavaScript — Iterators and Iterables

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript iterable objects.

Iterators that are Iterable

We can move the next function into its own object method if we return this in the Symbol.iterator method.

For instance, we can write:

function iterate(...args) {
  let index = 0;
  const iterable = {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      if (index < args.length) {
        return {
          value: args[index++]
        };
      } else {
        return {
          done: true
        };
      }
    },
  };
  return iterable;
}

We have the next within the iterable object we return.

The args have the items that we want to iterate over.

We return the object with the value while the index is less than args.length .

And we return an object with done set to true if there’s nothing left to iterate.

We return this inside the Symbo.iterator method so that the iteration can be done.

for-of loops work only with iterables and not the iterators directly.

Iterables always have the Symbol.iterator method, so we got to put our iterator over our iterable object to make it iterable.

return() and throw()

There’re 2 iterator methods that are optional.

return gives us an iterator the opportunity to clean if iterator ends early.

throw lets us forward a method call to a generator that’s iterated via yield* .

Closing Iterators

We can use return to close an iterator.

For instance, if we have the iterate function that we have before.

Then we can call break to end the for-of loop cleanly:

for (const x of iterate('foo', 'bar', 'baz')) {
  console.log(x);
  break;
}

return must return an object.

This is because of how generators handle the return statements.

Some constructors close iterators that aren’t completely clean up.

They include:

  • for-of
  • yield*
  • destructuring
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()
  • Promise.all(), Promise.race()

Combinators

Combinators are functions that combine existing iterables to create new ones.

For example, we can create one by writing:

function combinator(n, iterable) {
  const iter = iterable[Symbol.iterator]();
  return {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      if (0 < n) {
        n--;
        return iter.next();
      } else {
        return {
          done: true
        };
      }
    }
  };
}

We create a function to return the first n items from the iterable object.

Then we can use it by writing:

const arr = ['foo', 'bar', 'baz', 'qux'];
for (const x of combinator(3, arr)) {
  console.log(x);
}

This will log the first 3 items of the arr array.

Infinite Iterables

Iterable can return an infinite amount of values.

For instance, we can write:

function evenNums() {
  let n = 0;
  return {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      return {
        value: (++n) * 2
      };
    }
  }
}

to create an iterable object that returns even numbers.

Then we can create it by writing:

const nums = evenNums()
console.log(nums.next());
console.log(nums.next());
console.log(nums.next());

We call the evenNums function to create iterator.

Then we call next on each iterator to generate the numbers.

Conclusion

We can create iterable objects that return a finite and infinite number of values.

Categories
Modern JavaScript

Best of Modern JavaScript — Iterators and Generators

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript iterable objects and generator functions.

Clean Up of Generators

If we use break to break a loop, then we can clean up with the try-finally constructor.

For example, if we have:

function* genFn() {
  yield 'foo';
  yield 'bar';
  yield 'baz';
  console.log('clean up');

}

const gen = genFn();

for (const x of gen) {
  console.log(x);
}

Then the console log will run when the generator returned all the items.

However, if we use break in our loop:

for (const x of gen) {
  console.log(x);
  break;
}

Then the console log is never run.

We can run the cleanup step when we use break in our loop by wrapping the yield code with try-finally.

For example, we can write:

function* genFn() {
  try {
    yield 'foo';
    yield 'bar';
    yield 'baz';
  } finally {
    console.log('clean up');
  }
}

const gen = genFn();

for (const x of gen) {
  console.log(x);
  break;
}

We added the finally block, which will run when the break statement is run.

This is useful for running clean up code on our generator.

If we implement our own iterator, we can put the function in our iterator’s return method:

const obj = {
  [Symbol.iterator]() {
    function hasNextValue() {
      //...
    }

    function getNextValue() {
      //...
    }

    function cleanUp() {
      //...
    }

    return {
      next() {
        if (hasNextValue()) {
          const value = getNextValue();
          return {
            done: false,
            value: value
          };
        } else {
          //...
          return {
            done: true,
            value: undefined
          };
        }
      },
      return () {
        cleanUp();
      }
    };
  }
}

We have the return method included in the object we return with the Symbol.iterator method.

Closing Iterators

We can close any iterators by creating our own generator function to close it.

For instance, we can write:

function* take(n, iterable) {
  for (const x of iterable) {
    if (n <= 0) {
      break;
    }
    n--;
    yield x;
  }
}

We call break on the loop to end it with when the ending condition is met.

Generators

Generators are pieces of code that we can pause and resume.

It’s denoted by the function* keyword for generator functions.

yield is an operation that a generator used to pause itself.

Generators can receive input and send output with yield .

We can create a generator function by writing:

function* genFn() {
  yield 'foo';
  yield 'bar';
  yield 'baz';
}

This returns a generator object which we can call next on to return the values that are yield:

const genObj = genFn();
console.log(genObj.next());
console.log(genObj.next());
console.log(genObj.next());

We call genFn to return a generator.

Then we call next to get each value sequentially.

So we get:

{value: "foo", done: false}
{value: "bar", done: false}
{value: "baz", done: false}

We return an object with the value and done property.

The value has the value from yield .

done tells us whether all the values have been yield from the generator.

Conclusion

We can clean up our generator by adding a try-finally block.

Also, generator functions create generators that let us return the value sequentially.

Categories
Modern JavaScript

Best of Modern JavaScript — Iteration

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript iterable objects.

Speed of the Iteration Protocol

The speed of the iteration protocol has been taken into account when this is created.

Memory management is fast when managing small objects.

JavaScript engine optimization iteration so that no intermediate objects are allocated.

Reuse the Same Iterable Object

We can use iterable multiple times.

So we can write:

const results = [];
const iterator = [1, 2, 3][Symbol.iterator]();

while (!(val = iterator.next()).done) {
  results.push(val);
}

We get the iterator from the array.

And then we called it with our while loop to get the results.

Iteration

There’re rules that govern the JavaScript iteration protocol.

There are some rules for the next method.

As long as the iterator is returning a value next , returns an object with the value property.

And done will be false .

So we have:

{ value: x, done: false }

After the last value is iterated over, next should return an object whose property done is true .

Iterables that Return Iterators

Iterables can return fresh iterators or return the same iterator.

If they return fresh iterators, then each ones return values from the start.

So if we have something like and array and we use:

function getIterator(iterable) {
  return iterable[Symbol.iterator]();
}

to get the iterator, then we can compare them to see if they return the same iterator.

For instance, if we have:

const arr = ['a', 'b'];
console.log(getIterator(arr) === getIterator(arr));

Then the expression would log false .

This means even though the array is the same, they return the same iterator.

Other iterables like generators return the same iterator each time.

If we have generator objects, we return the same generator each time it’s called:

function* genFn() {
  yield 'foo';
  yield 'bar';
}

const gen = genFn();
console.log(getIterator(gen) === getIterator(gen));

The genFn is a generator function that returns a generator,

And when we get the iterator from the generator, we get the iterator from it and compare them and the expression logs true .

So the same generator has the same iterator.

We can iterate over a fresh iterator multiple times.

For instance, we can write:

const arr = ['foo', 'bar', 'baz'];

for (const a of arr) {
  console.log(a);
}

for (const a of arr) {
  console.log(a);
}

to loop through the same array twice.

On the other hand, if we have a generator:

function* genFn() {
  yield 'foo';
  yield 'bar';
}
const gen = genFn();

for (const a of gen) {
  console.log(a);
}

for (const a of gen) {
  console.log(a);
}

then we only loop through it once even if we have 2 loops.

Closing Iterators

There’re 2 ways that an iterator can be closed.

An iterator can be closed with exhaustion or closing.

Exhaustion is when the iterator returned all the iterable values.

Closing is done by calling return in the iterator function.

When we call return , then next won’t be called.

return is an optional method.

Not all iterators have it.

Iterators that have a return call is called closable.

return should only be called if an iterator hasn’t exhausted.

This can happen if we loop through the for-of loop with break , continue , return or throw .

return should produce an object that returns { done: true, value: x } .

Conclusion

Iterable objects can have different variations.

They can return a single iterator or multiple instances.

They can also be closable.

Categories
Modern JavaScript

Best of Modern JavaScript — Iterable Objects

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript iterable objects.

Iterable Computed Data

Iterable content can come from computed data.

Methods can return iterable objects that we can iterate through.

We have 3 methods that return iterable objects. They include the entries , keys , and values method.

entries returns an iterable over the entries encoded as key-value arrays.

For arrays, the keys are indexes and the values are the elements.

For sets, the keys and values are both the elements.

keys returns an iterable object with the keys of the entries.

values returns an iterable object with the values of the entries.

For instance, we can write:

const arr = ['foo', 'bar', 'baz'];
for (const [key, value] of arr.entries()) {
  console.log(key, value);
}

Then we get:

0 "foo"
1 "bar"
2 "baz"

logged.

Maps and Sets

The Map constructor turns an iterable key-value pairs into a map.

For instance, we can write:

const map = new Map([
  ['foo', 'one'],
  ['bar', 'two']
]);

Then we can call get to get the value.

For example, we can write:

const foo = map.get('foo');

and returns 'one' .

Likewise, we can pass in an array to the Set constructor.

For instance, we can write:

const set = new Set(['red', 'green', 'blue']);

Then we can use the has method to check whether the element exists.:

const hasRed = set.has('red');

WeakMaps and WeakSets work similarly.

Maps and sets are iterables themselves, but WeakMaps and WeakSets aren’t.

Promises

Promise.all and Promise.race takes iterables over promises.

So we can write:

Promise.all([
  Promise.resolve(1),
  Promise.resolve(2),
]).then(() => {
  //...
});

to run an array of promises in parallel.

Promise.race resolves to the value of the first promise from an array of promises.

For example, we can write:

Promise.race([
  Promise.resolve(1),
  Promise.resolve(2),
]).then(() => {
  //...
});

to resolve to the value of the first promise that finishes.

Implementing Iterables

JavaScript iterable objects have the Symbol.iterator method.

The method returns an object with the value and done properties.

value has the value we return from the iterable.

And done indicates whether there’re any values left. If it’s true then there’s no value left to return.

For example, we can write:

const obj = {
  [Symbol.iterator]() {
    let step = 0;
    const iterator = {
      next() {
        if (step <= 2) {
          step++;
        }
        switch (step) {
          case 1:
            return {
              value: 'foo', done: false
            };
          case 2:
            return {
              value: 'bar', done: false
            };
          default:
            return {
              value: undefined, done: true
            };
        }
      }
    };
    return iterator;
  }
};

The Symbol.iterator method returns an object according to the step number.

Then we can use the for-of loop to confirm that the object is iterable:

for (const x of obj) {
  console.log(x);
}

We should get:

foo
bar

logged.

We can omit done if it’s false and omit the value if it’s undefined .

So we can write:

switch (step) {
  case 1:
    return {
      value: 'foo'
    };
  case 2:
    return {
      value: 'bar'
    };
  default:
    return {
      done: true
    };
}

Conclusion

Iterable objects have special methods that distinguish them.

Categories
Modern JavaScript

Best of Modern JavaScript — Generators

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript generators.

Kinds of Generators

There’re various kinds of generators.

We can define generators as generator function declarations.

This takes the form of:

function* genFn() {
  //...
}
const genObj = genFn();

Generator function expressions are where we assign our generator function to a variable:

const genFn = function*() {
  //...
}
const genObj = genFn();

Generator methods in object literals let us create generator methods:

const obj = {
  * gen() {
    //...
  }
};
const genObj = obj.gen();

Generator methods can also be in classes.

For example, we can write:

class Foo {
  * gen() {
    //..,
  }
}
const foo = new Foo();
const genObj = foo.gen();

We create a class and add the gen method which is the generator method.

We can use generator functions to create our own iterable object.

For instance, we can write:

function* keys(obj) {
  const propKeys = Reflect.ownKeys(obj);

  for (const propKey of propKeys) {
    yield propKeys;
  }
}

const obj = {
  a: 1,
  b: 2
};

for (const key of keys(obj)) {
  console.log(key);
}

We create the keys generator function, which gets the keys from an object and returns the keys.

Then we loop through the keys with the for-of loop.

async and await also uses generators underneath,.

await works like yield in that it pauses the code until the result is retrieved.

The syntax also uses generators under the surface.

For instance, we can use it by writing:

async function fetchJson(url) {
  try {
    const request = await fetch(url);
    const res = await request.json();
    return res;
  } catch (error) {
    console.log(error);
  }
}

async functions only return promises.

The return statement returns a promise that resolves to the value returned.

Generators

Generators are functions that can be paused and resumed.

Generators are the only functions that can return generators.

So if we have a generator function, we can write:

function* keys(obj) {
  const propKeys = Reflect.ownKeys(obj);

  for (const propKey of propKeys) {
    yield propKeys;
  }
}

const obj = {
  a: 1,
  b: 2
};

const gen = keys(obj)

We call the keys genereator function to return a generator.

The generator is gen and ot’s not run until we call next .

For example, if we have:

const gen = keys(obj);

console.log(gen.next());
console.log(gen.next());

Then the generator function will run.

Uses of Generators

Generators can be used for iterators.

They produce data with yield and it can be accessed with the next method.

Generators can produce sequences of values with loops and recursion.

This means that generators can be used with the for-of and the spread operator.

They can also be data consumers.

yield can get values from the next method or any other sources.

They can also be both at the same time since they can be paused.

Conclusion

Generators can be data producers and consumers.

async and await also uses the generator syntax underneath the surface.