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.

Categories
Modern JavaScript

Best of Modern JavaScript — Generators as Producers

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.

Generators as Iterators

We can use generator functions as iterators.

For instance, we can write:

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

const gen = genFn();

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

to create a generator function and a generator.

genFn is the generator function as indicated with the function* keyword.

The function returns a generator with the genFn call.

Then we call next to get the next item on the list.

The yield keyword lets us return the value with the next method call.

Therefore, we get:

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

to get the yielded items via the value property.

We can also access the yielded values with the for-of loop.

For example, we can write:

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

const gen = genFn();

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

Then we get:

foo
bar
baz

logged.

The for-of loop can access the values from a generator.

The spread operator lets us convert a generator to an array by extracting the yielded values and putting it there:

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

const gen = genFn();
const arr = [...gen];

And destructuring can also work with generators by assigning the yielded values as values of the variables on the left side:

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

const [x, y] = genFn();

Then we get that x is 'foo' and y is 'bar' .

Returning from a Generator

We can use the return statement from a generator.

For instance, we can write:

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

Then we can invoke our generator by writing:

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

From the console log, we can see that the values are:

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

The return statement set done to true .

However, most other const5ructurs that work with iterable objects ignore the return value.

For instance, if we have the for-of loop:

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

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

Then we get:

foo
bar

logged.

And if we have the spread operator:

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

const gen = genFn();
const arr = [...gen];

Then arr is [“foo”, “bar”] .

Throwing Exception from a Generator

We can throw exceptions from a generator.

For instance, we can write:

function* genFn() {
  throw new Error('error');
}

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

We have a generator function that throws an error.

When we call it and then call next on the returned generator, we see the error logged in the console.

Conclusion

We can return items and throw errors in a generator function.

They can also produce values we can use.

Categories
Modern JavaScript

Best of Modern JavaScript — Generator Methods

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.

The First next()

The next call starts an observer.

The only purpose of the first invocation next is to start the observer.

So if we pass in a value to the first next call, it won’t be obtained by yield .

For example, we can write:

function* genFn() {
  while (true) {
    console.log(yield);
  }
}

const gen = genFn();
gen.next('a');
gen.next('b');
gen.next('c');

Then:

b
c

is logged.

The first call of next feeds 'a' to the generator, but there’s no way to receive it since there’s no yield statement.

Once it runs yield , then the value can be received.

yield ‘s operand is returned once next is called.

The returned value would be undefined since we don’t have an operand to go with it.

The 2nd invocation of next feeds 'b' into next , which is received by yield .

And that’s logged with the console log.

And next returned undefined value again because yield has no operand.

Therefore, we can only feed data to yield when next is called.

For example, we can write:

function* genFn() {
  while (true) {
    console.log(yield);
  }
}

const gen = genFn();
gen.next();
gen.next('a');
gen.next('b');
gen.next('c');

Then we get:

a
b
c

logged in the console.

yield Binds Loosely

yield treats the whole expression that’s after it as its operand.

For instance, if we have:

yield foo + bar + baz;

Then it’s treated as:

yield (foo + bar + baz);

rather than:

(yield foo) + bar + baz;

Therefore, we’ve to wrap our yield expressions with parentheses so that we can avoid syntax errors.

For example, instead of writing:

function* genFn() {
  console.log('yielded: ' + yield);
}

We write:

function* genFn() {
  console.log('yielded: ' + (yield));
}

return() and throw()

The return and throw methods are similar to next .

return lets us return something at the location of yield .

throw lets us throw an expression at the location of yield .

The next method is suspended at a yield operator.

The value from next is sent to yield .

return terminates the generator.

For example, we can write:

function* genFn() {
  try {
    console.log('start');
    yield;
  } finally {
    console.log('end');
  }
}

const gen = genFn();
gen.next()
gen.return('finished')

We have the genFn function that returns a generator with a given value.

The next method will run the generator.

And return ends the generator.

If we log the value of the return call, we get:

{value: "finished", done: true}

throw() lets us Throw Errors

We can throw errors with the throw keyword.

For instance, we can write:

function* genFn() {
  try {
    console.log('started');
    yield;
  } catch (error) {
    console.log(error);
  }
}

const gen = genFn();
gen.next();
console.log(gen.throw(new Error('error')));

We call throw with the generator function.

The catch block will be invoked once we call next to start the generator.

Then we’ll see:

Error: error

logged with the console log of the catch block.

And {value: undefined, done: true} is returned from the throw call.

Conclusion

We can call the next method and use it with the yield operator without an operand.

This will take the value from next and return it.

Generators also have the return and throw methods to end the generator and throw errors.

Categories
Modern JavaScript

Best of Modern JavaScript — Generator Functions

ince 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.

Implementing Iterables via Generators

Iterables can be implemented with generators.

We can create an iterable object with the Symbol.iterable method to make it iterable.

For instance, we can write:

const obj = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  }
}

We have the yield keyword in our Symbol.iterator method.

Then we can write:

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

to iterate through the values, and we get:

1
2
3

obj[Symbol.iterator] is a generator method that yields the values we can iterate through.

for-of use the method as an iterator to get the values.

Infinite Iterables

We can make itrerables that are infinite with generators.

For instance, we can write:

const obj = {
  *[Symbol.iterator]() {
    for (let n = 0;; n++) {
      yield n;
    }
  }
}

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

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

Our iterable obj object has the Symbol.iterator method that yields integers.

Then we created the take function to yield the first n items from the iterable .

And finally, we loop through the returned variables.

Generators for Lazy Evaluation

Generators are useful for lazy evaluation.

For instance, we can create a function that yields the individual characters from a string.

We loop through the string and yield the keys.

For example, we can write:

function* tokenize(str) {
  for (const s of str) {
    yield s;
  }
}

let c;
const gen = tokenize('foobar');

while (c = gen.next().value) {
  console.log(c);
}

and we can get the values from the object.

Inheritance and Iterators

We can implement inheritance with generator functions like any other function.

For example, we can add an instance method to our generator by writing:

function* g() {}
g.prototype.foo = function() {
  return 'bar'
};
const obj = g();
console.log(obj.foo());

We added the foo method to the prototype property.

Then we call g generator function return the generator.

Then we can call the foo method on it.

The console log should have 'bar' logged.

If we try to get the prototype of an iterator.

For instance, we can write:

const getProto = Object.getPrototypeOf.bind(Object);
console.log(getProto([][Symbol.iterator]()));

Then we see the Array Iterator object.

It has the next method and other properties of the iterator’s prototype.

this in Generators

Generator functions have their own value of this .

It’s a function that sets up and returns a generator object.

And it contains the code that the generator object steps through.

We can look at the value of this by writing:

function* gen() {
  yield this;
}

const [genThis] = gen();
console.log(genThis)

If it’s at the top level and we get the value of this as we did with destructuring, we get the window object.

This is assuming that strict mode is off.

If strict mode is on:

function* gen() {
  'use strict';
  yield this;
}

then genThis is undefined .

If our generator is in an object:

function* gen() {
  'use strict';
  yield this;
}

const obj = {
  gen
}

const [genThis] = obj.gen();
console.log(genThis);

The genThis is the object itself.

Generator functions are like traditional functions.

Conclusion

Iterables can be implemented with generators.

Also, we can implement inheritance and get the value of this with generator functions.