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.

Categories
Modern JavaScript

Best of Modern JavaScript — Generator Best Practices

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 and new regex features.

The Asterisk

The asterisk for generator functions has an asterisk.

If must be between the function keyword and the function name.

And it can have any kind of spacing.

But usually, it has this format:

function* gen() {
  //..
}

Generator Function Declarations and Expressions

Generator function declarations and expressions are both valid.

We can write generator function declarations by writing:

function* gen() {
  //..
}

And we can create generator expressions by assigning a function declaration to a variable:

const gen = function* () {
  //..
}

Generator Method Definitions

We can add generator method definitions by writing:

const obj = {
  * gen(x, y) {
    //...
  }
};

We have the asterisk before the method name.

This is a shorthand for the following:

const obj = {
  gen: function*(x, y) {
    //...
  }
};

Generator method definitions are similar to getters and setters.

For example, we can write:

const obj = {
  get foo() {
    //...
  },
  set foo(value) {
    //...
  }
};

Like the asterisk, get and set are modifiers for the foo methods.

Recursive yield

We can add an asterisk to yield to call another generator function from a generator function.

For instance, we can write:

function* foo(x) {
  //...
  yield* foo(x - 1);
  //..
}

We recursively call foo with the yield* keyword.

Why Use the function* Keyword for Generator Functions?

The function* keyword is chosen for generator functions because the keyword generator might be used by something else.

yield

yield is a reserved word in strict mode.

So it can’t be used with it on.

If the ES6 code is in sloppy mode, then the keyword becomes a contextual keyword that’s available only inside generators.

New Regex Features

New regex features include the /y flag to let us anchor the starting point for the search to the lastIndex property of a regex object.

This is similar to the ^ anchor, but the match for ^ always starts from 0.

This means the match is useful for use with the g flag.

We can use it to match a pattern multiple times.

It’s also useful to find matches immediately after its predecessor.

For example, we can use the regex.prototype.exec method to find all the matches from a string.

If we have:

const REGEX = /foo/;

REGEX.lastIndex = 8;
const match = REGEX.exec('barfoobarfoo');
console.log(match.index);
console.log(REGEX.lastIndex);

Then index is ignores the lastIndex property of the regex, but if we add the g flag:

const REGEX = /foo/g;

REGEX.lastIndex = 8;
const match = REGEX.exec('barfoobarfoo');
console.log(match.index);
console.log(REGEX.lastIndex);

The pattern search starts with the lastIndex index value.

If y is set:

const REGEX = /foo/y;

REGEX.lastIndex = 9;
const match = REGEX.exec('barfoobarfoo');
console.log(match.index);
console.log(REGEX.lastIndex);

If we set the lastIndex to 9, then we get the 'foo' match with that index.

The index has to the exact start point of the match for the y flag to pick up the match.

lastIndex will be updated to 12 after the match is found.

Setting /g and /y together is the same as setting y .

Conclusion

Generator code style can vary.

Also, JavaScript regex objects now can use the y flag to find the exact match after the flag.

Categories
Modern JavaScript

Best of Modern JavaScript — for-of Loop

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 Data Sources

We can use the for-of loop to iterate through various kinds of iterable objects.

For example, we can loop through an array by writing:

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

Then we get:

foo
bar
baz

logged.

We can also use it with strings.

For example, we can write:

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

Then we get the letters of 'foo' logged individually.

Maps are also itrerable objects, so we can use it with the for-of loop.

For instance, we can write:

const map = new Map().set('foo', 1).set('bar', 2);
for (const \[key, value\] of map) {
  console.log(key, value);
}

We created a map with some keys and values with the Map constructor.

Then in the for-of loop, we looped through each entry and destructured the key and value from the map entries.

So we get:

foo 1
bar 2

from the console log.

Sets are also iterables that can be iterated through.

For example, we can write:

const set = new Set().add('foo').add('bar');
for (const x of set) {
  console.log(x);
}

We created a set and added some items to it.

Then we used the for-of loop with it.

And so we get:

foo
bar

from the console log.

arguments is an object that’s available inside traditional functions to get the arguments from a function call.

We can use it with the for-of loop as it’s an iterable object:

function logArgs() {
  for (const x of arguments) {
    console.log(x);
  }
}
logArgs('foo', 'bar');

We passed in 'foo' and 'bar' to the logArgs function and we get the values from the for-of loop.

DOM NodeLists can also be iterated through with the for-of loop.

For instance, we can write:

for (const div of document.querySelectorAll('div')) {
  console.log(div);
}

to loop through all the divs on the page.

Iterable Computed Data

If a function returns an iterable object, we can loop through it with the for-of loop.

For instance, we can write:

const arr = \['a', 'b', 'c'\];
for (const \[index, element\] of arr.entries()) {
  console.log(index, element);
}

Then we get:

0 "a"
1 "b"
2 "c"

We get the indexes and the elements with the arr.entries method and we get the values each entry inside the loop body.

Plain Objects are not Iterable

Plain objects aren’t iterable, so we can’t use them with the for-of loop.

For instance, something like:

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

would give us the ‘Uncaught TypeError: {} is not iterable’ error.

Objects aren’t iterable because either we loop through the properties of an object to examine its structure.

Or we loop through the data.

This should be kept separate to avoid confusing objects with other kinds of data structures.

If we want to make an object iterable, we can create the Symbol.iterator generator method to make it so.

Conclusion

The for-of loop can iterate through many kinds of iterable objects.