Tuesday, April 2, 2019

JavaScript Generators and How They Work

What Are Generators?

  • Generators in ES6 are a new way of working with functions and iterators.
  • They appear to be a function but behaves like an iterator which simplifies the task of writing iterators.
  • They are different from a normal function, as they can stop mid-execution and then continue from where they stopped.
  • In layman’s terms, they are similar to the way we put a marker/bookmark in a page of the book we are reading. Then later we resume reading the book from that particular page instead of starting all over again. Here we acted as a generator function.
  • They can generate a series of results instead of a single value.

Generator Syntax

  • Creating a generator is similar to a normal function, with the difference that it requires a * between the function keyword and function name.
  • It allows any number of spaces or no spaces in between.
  • It uses the yield keyword to pause and resume execution.
  • Since it is just a function, we can use it anywhere just like we would with normal function, i.e. inside objects and class methods.
  • Basic syntax:
// All below formats are valid with spaces/no spaces in between function keyword, * and function name.
function * functionName() {
    // Your generator related codes here.
}

// OR below format.
function *functionName() {
    // Your generator related codes here.
}

// OR below format.
function* functionName() {
    // Your generator related codes here.
}

// OR below format.
function  *  functionName() {
    // Your generator related codes here.
}

// OR below format.

function*functionName() {
    // Your generator related codes here.
}

// Inside Class method.
class testClass {
    *functionName() {}
}

// Inside object
var testObject = {
    *functionName() {}
}

Yield Keyword

  • The yield keyword is used to return data and pause the execution of the generator.
  • Every time the generator encounters yield, it halts the execution at that point and returns the specified value. It then resumes the execution upon calling the next() method on the Generator object until it finds the next yield.
  • In the context of generators, we do not say that it returned a value. We say that it has yielded the value.
  • Using return inside a generator will set the done property to true, after which it cannot generate any more values. All the code after the return statement will not execute.
  • Syntax:
// Generator with yield. 
function * generator() {
    console.log('First execution.');
    yield 'Yield First execution, saved the state and exit';
    console.log('Second execution will be on next() call.');  
    yield 'Yield second execution, saved the state and exit';
}

const generatorObj = generator();

next() in Generators

  • The generator returns an object on which we can call next().
  • Every invocation of next() will return an object containing the returned value and the status of the generator's execution.
  • The value property will contain the value and the done property will return true or false for the generator’s execution status.
  • When done becomes true, the generator stops and won’t generate any more values.
{
    value: Any,
    done: true|false
}
function * generator() {
    console.log('First execution.');
    yield 'Yield First execution, saved the state and exit';
    console.log('Second execution will be on next() call.');  
    yield 'Yield second execution, saved the state and exit';
}

const generatorObj = generator();

// After below execution, generator will yield value, return below object and pause execution.
// {value: 'Yield First execution, saved the state and exit', done: false}
setTimeout(() => console.log(generatorObj.next()), 1000);

// After below execution, generator will yield value, return below object and pause execution.
// {value: 'Yield First execution, saved the state and exit', done: false} 
setTimeout(() => console.log(generatorObj.next()), 3000);

// After below execution, generator will yield value, return below object and close execution.
// {value: undefined, done: true}
setTimeout(() => console.log(generatorObj.next()), 5000);
  • The below screenshot shows the different states of a generator during its execution of how it gets "suspended" when it encounters the yield keyword, resumes when next() is called, and then gets closed after the execution is complete:JavaScript generators
  • If we use return then it will not execute any code present after the return statement. Sample code (JSFiddle link: generator with return statement):
function * generator() {
    console.log('First execution.');
    yield 'Yield First execution, saved the state and exit';
    return 'Execution ends here due to return and will not execute rest of the codes.';  
    // All below codes will not get executed.
    yield 'Yield second execution, saved the state and exit';
}

const generatorObj = generator();

// After below execution, generator will yield value, return below object and pause execution.
// {value: 'Yield First execution, saved the state and exit', done: false}
setTimeout(() => console.log(generatorObj.next()), 1000);

// After below execution, generator will yield value, return below object and completes the execution.
// {value: 'Execution ends here due to return and will not execute rest of the codes.', done: true}
setTimeout(() => console.log(generatorObj.next()), 3000);

// generator execution is already completed so it will just return value as undefined and done as true.
// {value: undefined, done: true}
setTimeout(() => console.log(generatorObj.next()), 5000);
function * generator() {
    console.log('First execution.');
    yield 'Yield First execution, saved the state and exit';
    console.log('Second execution will be on next() call.');  
    let passedArgument = yield 'Yield second execution, saved the state and exit';
    yield passedArgument;
}

const generatorObj = generator();

// After below execution, generator will yield value, return below object and pause execution.
// {value: 'Yield First execution, saved the state and exit', done: false}
setTimeout(() => console.log(generatorObj.next()), 1000);

// After below execution, generator will yield value, return below object and pause execution.
// {value: 'Yield First execution, saved the state and exit', done: false} 
setTimeout(() => console.log(generatorObj.next()), 3000);

// After below execution, generator will yield value, return below object and pause execution.
// {value: "This is the passed value", done: false}
setTimeout(() => console.log(generatorObj.next('This is the passed value')), 5000);

// After below execution, generator will yield value, return below object and close execution.
// {value: undefined, done: true}
setTimeout(() => console.log(generatorObj.next()), 7000);

Why Use Generators

  • Implementing Iterables:
    • Implementation of an iterator in JavaScript requires things to be done manually, like:
      • Creating an iterator object.
      • Implemeting the next() method.
      • Making a return object for next() in the proper format: { value: Any, done: true | false }
      • Saving the state.
    • The below sample code shows how hard it is to manually implement everything in an iterator.
    const iterableObj = {
        [Symbol.iterator]() {
            var counter = 0;
            return {
                next() {
                    counter++;
                        if (counter === 1) {
                            return { value: 'step one', done: false};
                        } else if (counter === 2) {
                            return { value: 'step two', done: false};
                        } else if (counter === 3) {
                            return { value: 'step three', done: false};
                        }
                    return { value: '', done: true };
                }
            }
        },
    }
    
    for (const val of iterableObj) {
        console.log(val);
    }
    
    • The below sample code shows how easy it is with a generator which handles most of the things on its own.
    function * iterableObj() {
        yield 'step one';
        yield 'step two';
        yield 'step three';
    }
    
    for (const val of iterableObj()) {
      console.log(val);
    }
    
  • Lazy execution: Generator helps to delay the evaluation of an expression until its value is required. So, if we don’t need a value, it won’t exist and is calculated only when we need it.
  • Memory efficient: It is memory efficient because we generate only the values that are required. We need to pre-generate all the values in case of normal functions and keep them in case we can use them later. But generators can defer the computation of any expressions/code until we need it.

Limitations

  • Generators are one-time access only. Once you’ve exhausted all the values, you can’t iterate over it again. To generate the values again, you need to make a new generator object.
  • Generators do not allow random access with arrays. 

I hope this post has helped you get a better grasp on  JavaScript generators and their basic functionalities.

Happy coding!



from DZone.com Feed https://ift.tt/2UaK1Em

No comments:

Post a Comment