One place for hosting & domains

      Understanding

      Understanding Template Literals in JavaScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      The 2015 edition of the ECMAScript specification (ES6) added template literals to the JavaScript language. Template literals are a new form of making strings in JavaScript that add a lot of powerful new capabilities, such as creating multi-line strings more easily and using placeholders to embed expressions in a string. In addition, an advanced feature called tagged template literals allows you to perform operations on the expressions within a string. All of these capabilities increase your options for string manipulation as a developer, letting you generate dynamic strings that could be used for URLs or functions that customize HTML elements.

      In this article, you will go over the differences between single/double-quoted strings and template literals, running through the various ways to declare strings of different shape, including multi-line strings and dynamic strings that change depending on the value of a variable or expression. You will then learn about tagged templates and see some real-world examples of projects using them.

      Declaring Strings

      This section will review how to declare strings with single quotes and double quotes, and will then show you how to do the same with template literals.

      In JavaScript, a string can be written with single quotes (' '):

      const single="Every day is a good day when you paint."
      

      A string can also be written with double quotes (" "):

      const double = "Be so very light. Be a gentle whisper."
      

      There is no major difference in JavaScript between single- or double-quoted strings, unlike other languages that might allow interpolation in one type of string but not the other. In this context, interpolation refers to the evaluation of a placeholder as a dynamic part of a string.

      The use of single- or double-quoted strings mostly comes down to personal preference and convention, but used in conjunction, each type of string only needs to escape its own type of quote:

      // Escaping a single quote in a single-quoted string
      const single=""We don"t make mistakes. We just have happy accidents." - Bob Ross'
      
      // Escaping a double quote in a double-quoted string
      const double = ""We don't make mistakes. We just have happy accidents." - Bob Ross"
      
      console.log(single);
      console.log(double);
      

      The result of the log() method here will print the same two strings to the console:

      Output

      "We don't make mistakes. We just have happy accidents." - Bob Ross "We don't make mistakes. We just have happy accidents." - Bob Ross

      Template literals, on the other hand, are written by surrounding the string with the backtick character, or grave accent (`):

      const template = `Find freedom on this canvas.`
      

      They do not need to escape single or double quotes:

      const template = `"We don't make mistakes. We just have happy accidents." - Bob Ross`
      

      However, they do still need to escape backticks:

      const template = `Template literals use the ` character.`
      

      Template literals can do everything that regular strings can, so you could possibly replace all strings in your project with them and have the same functionality. However, the most common convention in codebases is to only use template literals when using the additional capabilities of template literals, and consistently using the single or double quotes for all other simple strings. Following this standard will make your code easier to read if examined by another developer.

      Now that you’ve seen how to declare strings with single quotes, double quotes, and backticks, you can move on to the first advantage of template literals: writing multi-line strings.

      Multi-line Strings

      In this section, you will first run through the way strings with multiple lines were declared before ES6, then see how template literals make this easier.

      Originally, if you wanted to write a string that spans multiple lines in your text editor, you would use the concatenation operator. However, this was not always a straight-forward process. The following string concatenation seemed to run over multiple lines:

      const address="Homer J. Simpson" + 
        '742 Evergreen Terrace' + 
        'Springfield'
      

      This might allow you to break up the string into smaller lines and include it over multiple lines in the text editor, but it has no effect on the output of the string. In this case, the strings will all be on one line and not separated by newlines or spaces. If you logged address to the console, you would get the following:

      Output

      Homer J. Simpson742 Evergreen TerraceSpringfield

      You can use the backslash character () to continue the string onto multiple lines:

      const address="Homer J. Simpson
        742 Evergreen Terrace
        Springfield"
      

      This will retain any indentation as whitespace, but the string will still be on one line in the output:

      Output

      Homer J. Simpson 742 Evergreen Terrace Springfield

      Using the newline character (n), you can create a true multi-line string:

      const address="Homer J. Simpsonn" + 
        '742 Evergreen Terracen' + 
        'Springfield'
      

      When logged to the console, this will display the following:

      Output

      Homer J. Simpson 742 Evergreen Terrace Springfield

      Using newline characters to designate multi-line strings can be counterintuitive, however. In contrast, creating a multi-line string with template literals can be much more straight-forward. There is no need to concatenate, use newline characters, or use backslashes. Just pressing ENTER and writing the string across multiple lines works by default:

      const address = `Homer J. Simpson
      742 Evergreen Terrace
      Springfield`
      

      The output of logging this to the console is the same as the input:

      Output

      Homer J. Simpson 742 Evergreen Terrace Springfield

      Any indentation will be preserved, so it’s important not to indent any additional lines in the string if that is not desired. For example, consider the following:

      const address = `Homer J. Simpson
                       742 Evergreen Terrace
                       Springfield`
      

      Although this style of writing the line might make the code more human readable, the output will not be:

      Output

      Homer J. Simpson 742 Evergreen Terrace Springfield

      With multi-line strings now covered, the next section will deal with how expressions are interpolated into their values with the different string declarations.

      Expression Interpolation

      In strings before ES6, concatenation was used to create a dynamic string with variables or expressions:

      const method = 'concatenation'
      const dynamicString = 'This string is using ' + method + '.'
      

      When logged to the console, this will yield the following:

      Output

      This string is using concatenation.

      With template literals, an expression can be embedded in a placeholder. A placeholder is represented by ${}, with anything within the curly brackets treated as JavaScript and anything outside the brackets treated as a string:

      const method = 'interpolation'
      const dynamicString = `This string is using ${method}.`
      

      When dynamicString is logged to the console, the console will show the following:

      Output

      This string is using interpolation.

      One common example of embedding values in a string might be for creating dynamic URLs. With concatenation, this can be cumbersome. For example, the following declares a function to generate an OAuth access string:

      function createOAuthString(host, clientId, scope) {
        return host + '/login/oauth/authorize?client_id=' + clientId + '&scope=" + scope
      }
      
      createOAuthString("https://github.com', 'abc123', 'repo,user')
      

      Logging this function will yield the following URL to the console:

      Output

      https://github.com/login/oauth/authorize?client_id=abc123&scope=repo,user

      Using string interpolation, you no longer have to keep track of opening and closing strings and concatenation operator placement. Here is the same example with template literals:

      function createOAuthString(host, clientId, scope) {
        return `${host}/login/oauth/authorize?client_id=${clientId}&scope=${scope}`
      }
      
      createOAuthString('https://github.com', 'abc123', 'repo,user')
      

      This will have the same output as the concatenation example:

      Output

      https://github.com/login/oauth/authorize?client_id=abc123&scope=repo,user

      You can also use the trim() method on a template literal to remove any whitespace at the beginning or end of the string. For example, the following uses an arrow function to create an HTML <li> element with a customized link:

      const menuItem = (url, link) =>
        `
      <li>
        <a href="https://www.digitalocean.com/${url}">${link}</a>
      </li>
      `.trim()
      
      menuItem('https://google.com', 'Google')
      

      The result will be trimmed of all the whitespace, ensuring that the element will be rendered correctly:

      Output

      <li> <a href="https://google.com">Google</a> </li>

      Entire expressions can be interpolated, not just variables, such as in this example of the sum of two numbers:

      const sum = (x, y) => x + y
      const x = 5
      const y = 100
      const string = `The sum of ${x} and ${y} is ${sum(x, y)}.`
      
      console.log(string)
      

      This code defines the sum function and the variables x and y, then uses both the function and the variables in a string. The logged result will show the following:

      Output

      The sum of 5 and 100 is 105.

      This can be particularly useful with ternary operators, which allow conditionals within a string:

      const age = 19
      const message = `You can ${age < 21 ? 'not' : ''} view this page`
      console.log(message)
      

      The logged message here will change depnding on whether the value of age is over or under 21. Since it is 19 in this example, the following output will be logged:

      Output

      You can not view this page

      Now you have an idea of how template literals can be useful when used to interpolate expressions. The next section will take this a step further by examining tagged template literals to work with the expressions passed into placeholders.

      Tagged Template Literals

      An advanced feature of template literals is the use of tagged template literals, sometimes referred to as template tags. A tagged template starts with a tag function that parses a template literal, allowing you more control over manipulating and returning a dynamic string.

      In this example, you’ll create a tag function to use as the function operating on a tagged template. The string literals are the first parameter of the function, named strings here, and any expressions interpolated into the string are packed into the second parameter using rest parameters. You can console out the parameter to see what they will contain:

      function tag(strings, ...expressions) {
        console.log(strings)
        console.log(expressions)
      }
      

      Use the tag function as the tagged template function and parse the string as follows:

      const string = tag`This is a string with ${true} and ${false} and ${100} interpolated inside.`
      

      Since you’re console logging strings and expressions, this will be the output:

      Output

      (4) ["This is a string with ", " and ", " and ", " interpolated inside." (3) [true, false, 100]

      The first parameter, strings, is an array containing all the string literals:

      • "This is a string with "
      • " and "
      • " and "
      • " interpolated inside."

      There is also a raw property available on this argument at strings.raw, which contains the strings without any escape sequences being processed. For example, /n would just be the character /n and not be escaped into a newline.

      The second parameter, ...expressions, is a rest parameter array consisting of all the expressions:

      The string literals and expressions are passed as parameters to the tagged template function tag. Note that the tagged template does not need to return a string; it can operate on those values and return any type of value. For example, we can have the function ignore everything and return null, as in this returnsNull function:

      function returnsNull(strings, ...expressions) {
        return null
      }
      
      const string = returnsNull`Does this work?`
      console.log(string)
      

      Logging the string variable will return:

      Output

      null

      An example of an action that can be performed in tagged templates is applying some change to both sides of each expression, such as if you wanted to wrap each expression in an HTML tag. Create a bold function that will add <strong> and </strong> to each expression:

      function bold(strings, ...expressions) {
        let finalString = ''
      
        // Loop through all expressions
        expressions.forEach((value, i) => {
          finalString += `${strings[i]}<strong>${value}</strong>`
        })
      
        // Add the last string literal
        finalString += strings[strings.length - 1]
      
        return finalString
      }
      
      const string = bold`This is a string with ${true} and ${false} and ${100} interpolated inside.`
      
      console.log(string)
      

      This code uses the forEach method to loop over the expressions array and add the bolding element:

      Output

      This is a string with <strong>true</strong> and <strong>false</strong> and <strong>100</strong> interpolated inside.

      There are a few examples of tagged template literals in popular JavaScript libraries. The graphql-tag library uses the gql tagged template to parse GraphQL query strings into the abstract syntax tree (AST) that GraphQL understands:

      import gql from 'graphql-tag'
      
      // A query to retrieve the first and last name from user 5
      const query = gql`
        {
          user(id: 5) {
            firstName
            lastName
          }
        }
      `
      

      Another library that uses tagged template functions is styled-components, which allows you to create new React components from regular DOM elements and apply additional CSS styles to them:

      import styled from 'styled-components'
      
      const Button = styled.button`
        color: magenta;
      `
      
      // <Button> can now be used as a custom component
      

      You can also use the built-in String.raw method on tagged template literals to prevent any escape sequences from being processed:

      const rawString = String.raw`I want to write /n without it being escaped.`
      console.log(rawString)
      

      This will log the following:

      Output

      I want to write /n without it being escaped.

      Conclusion

      In this article, you reviewed single- and double-quoted string literals and you learned about template literals and tagged template literals. Template literals make a lot of common string tasks simpler by interpolating expressions in strings and creating multi-line strings without any concatenation or escaping. Template tags are also a useful advanced feature of template literals that many popular libraries have used, such as GraphQL and styled-components.

      To learn more about strings in JavaScript, read How To Work with Strings in JavaScript and How To Index, Split, and Manipulate Strings in JavaScript.



      Source link

      Understanding Default Parameters in JavaScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      In ECMAScript 2015, default function parameters were introduced to the JavaScript language. These allow developers to initialize a function with default values if the arguments are not supplied to the function call. Initializing function parameters in this way will make your functions easier to read and less error-prone, and will provide default behavior for your functions. This will help you avoid errors that stem from passing in undefined arguments and destructuring objects that don’t exist.

      In this article, you will review the difference between parameters and arguments, learn how to use default parameters in functions, see alternate ways to support default parameters, and learn what types of values and expressions can be used as default parameters. You will also run through examples that demonstrate how default parameters work in JavaScript.

      Arguments and Parameters

      Before explaining default function parameters, it is important to know what it is that parameters can default to. Because of this, we will first review the difference between arguments and parameters in a function. If you would like to learn more about this distinction, check out our earlier article in the JavaScript series, How to Define Functions in JavaScript.

      In the following code block, you will create a function that returns the cube of a given number, defined as x:

      // Define a function to cube a number
      function cube(x) {
        return x * x * x
      }
      

      The x variable in this example is a parameter—a named variable passed into a function. A parameter must always be contained in a variable and must never have a direct value.

      Now take a look at this next code block, which calls the cube function you just created:

      // Invoke cube function
      cube(10)
      

      This will give the following output:

      Output

      1000

      In this case, 10 is an argument—a value passed to a function when it is invoked. Often the value will be contained in a variable as well, such as in this next example:

      // Assign a number to a variable
      const number = 10
      
      // Invoke cube function
      cube(number)
      

      This will yield the same result:

      Output

      1000

      If you do not pass an argument to a function that expects one, the function will implicitly use undefined as the value:

      // Invoke the cube function without passing an argument
      cube()
      

      This will return:

      Output

      NaN

      In this case, cube() is trying to calculate the value of undefined * undefined * undefined, which results in NaN, or “not a number”. For more on this, take a look at the number section of Understanding Data Types in JavaScript.

      This automatic behavior can sometimes be a problem. In some cases, you might want the parameter to have a value even if no argument was passed to the function. That’s where the default parameters feature comes in handy, a topic that you will cover in the next section.

      Default Parameter Syntax

      With the addition of default parameters in ES2015, you can now assign a default value to any parameter, which the function will use instead of undefined when called without an argument. This section will first show you how to do this manually, and then will guide you through setting default parameters.

      Without default parameters, you would have to explicitly check for undefined values in order to set defaults, as is shown in this example:

      // Check for undefined manually
      function cube(x) {
        if (typeof x === 'undefined') {
          x = 5
        }
      
        return x * x * x
      }
      
      cube()
      

      This uses a conditional statement to check if the value has been automatically provided as undefined, then sets the value of x as 5. This will result in the following output:

      Output

      125

      In contrast, using default parameters accomplishes the same goal in much less code. You can set a default value to the parameter in cube by assigning it with the equality assignment operator (=), as highlighted here:

      // Define a cube function with a default value
      function cube(x = 5) {
        return x * x * x
      }
      

      Now when the cube function is invoked without an argument, it will assign 5 to x and return the calculation instead of NaN:

      // Invoke cube function without an argument
      cube()
      

      Output

      125

      It will still function as intended when an argument is passed, ignoring the default value:

      // Invoke cube function with an argument
      cube(2)
      

      Output

      8

      However, one important caveat to note is that the default parameter value will also override an explicit undefined passed as an argument to a function, as demonstrated here:

      // Invoke cube function with undefined
      cube(undefined)
      

      This will give the calculation with x equal to 5:

      Output

      125

      In this case, the default parameter values were calculated, and an explicit undefined value did not override them.

      Now that you have an idea of the basic syntax of default parameters, the next section will show how default parameters work with different data types.

      Default Parameter Data Types

      Any primitive value or object can be used as a default parameter value. In this section, you will see how this flexibility increases the ways in which default parameters can be used.

      First, set parameters using a number, string, boolean, object, array, and null value as a default value. This example will use arrow function syntax:

      // Create functions with a default value for each data type
      const defaultNumber = (number = 42) => console.log(number)
      const defaultString = (string = 'Shark') => console.log(string)
      const defaultBoolean = (boolean = true) => console.log(boolean)
      const defaultObject = (object = { id: 7 }) => console.log(object)
      const defaultArray = (array = [1, 2, 3]) => console.log(array)
      const defaultNull = (nullValue = null) => console.log(nullValue)
      

      When these functions are invoked without parameters, they will all use the default values:

      // Invoke each function
      defaultNumber()
      defaultString()
      defaultBoolean()
      defaultObject()
      defaultArray()
      defaultNull()
      

      Output

      42 "Shark" true {id: 7} (3) [1, 2, 3] null

      Note that any object created in a default parameter will be created every time the function is called. One of the common use cases for default parameters is to use this behavior to obtain values out of an object. If you try to destructure or access a value from an object that doesn’t exist, it will throw an error. However, if the default parameter is an empty object, it will simply give you undefined values instead of throwing an error:

      // Define a settings function with a default object
      function settings(options = {}) {
        const { theme, debug } = options
      
        // Do something with settings
      }
      

      This will avoid the error caused by destructuring objects that don’t exist.

      Now that you’ve seen how default parameters operate with different data types, the next section will explain how multiple default parameters can work together.

      Using Multiple Default Parameters

      You can use as many default parameters as you want in a function. This section will show you how to do this, and how to use it to manipulate the DOM in a real-world example.

      First, declare a sum() function with multiple default parameters:

      // Define a function to add two values
      function sum(a = 1, b = 2) {
        return a + b
      }
      
      sum()
      

      This will result in the following default calculation:

      Output

      3

      Additionally, the value used in a parameter can be used in any subsequent default parameter, from left to right. For example, this createUser function creates a user object userObj as the third parameter, and all the function itself does is return userObj with the first two parameters:

      // Define a function to create a user object using parameters
      function createUser(name, rank, userObj = { name, rank }) {
        return userObj
      }
      
      // Create user
      const user = createUser('Jean-Luc Picard', 'Captain')
      

      If you call user here, you will get the following:

      Output

      {name: "Jean-Luc Picard", rank: "Captain"}

      It is usually recommended to put all default parameters at the end of a list of parameters, so that you can easily leave off optional values. If you use a default parameter first, you will have to explicitly pass undefined to use the default value.

      Here is an example with the default parameter at the beginning of the list:

      // Define a function with a default parameter at the start of the list
      function defaultFirst(a = 1, b) {
        return a + b
      }
      

      When calling this function, you would have to call defaultFirst() with two arguments:

      defaultFirst(undefined, 2)
      

      This would give the following:

      Output

      3

      Here is an example with the default parameter at the end of the list:

      // Define a function with a default parameter at the end of the list
      function defaultLast(a, b = 1) {
        return a + b
      }
      
      defaultLast(2)
      

      This would yield the same value:

      Output

      3

      Both functions have the same result, but the one with the default value last allows a much cleaner function call.

      For a real-world example, here is a function that will create a DOM element, and add a text label and classes, if they exist.

      // Define function to create an element
      function createNewElement(tag, text, classNames = []) {
        const el = document.createElement(tag)
        el.textContent = text
      
        classNames.forEach(className => {
          el.classList.add(className)
        })
      
        return el
      }
      

      You can call the function with some classes in an array:

      const greeting = createNewElement('p', 'Hello!', ['greeting', 'active'])
      

      Calling greeting will give the following value:

      Output

      <p class="greeting active">Hello!</p>

      However, if you leave the classNames array out of the function call, it will still work.

      const greeting2 = createNewElement('p', 'Hello!')
      

      greeting2 now has the following value:

      Output

      <p>Hello!</p>

      In this example, forEach() can be used on an empty array without an issue. If that empty array were not set in the default parameter, you would get the following error:

      Output

      VM2673:5 Uncaught TypeError: Cannot read property 'forEach' of undefined at createNewElement (<anonymous>:5:14) at <anonymous>:12:18

      Now that you have seen how multiple default parameters can interact, you can move on to the next section to see how function calls work as default parameters.

      Function Calls as Default Parameters

      In addition to primitives and objects, the result of calling a function can be used as a default parameter.

      In this code block, you will create a function to return a random number, and then use the result as the default parameter value in a cube function:

      // Define a function to return a random number from 1 to 10
      function getRandomNumber() {
        return Math.floor(Math.random() * 10)
      }
      
      // Use the random number function as a default parameter for the cube function
      function cube(x = getRandomNumber()) {
        return x * x * x
      }
      

      Now invoking the cube function without a parameter will have potentially different results every time you call it:

      // Invoke cube function twice for two potentially different results
      cube()
      cube()
      

      The output from these function calls will vary:

      Output

      512 64

      You can even use built-in methods, like those on the Math object, and use the value returned in one function call as a parameter in another function.

      In the following example, a random number is assigned to x, which is used as the parameter in the cube function you created. The y parameter will then calculate the cube root of the number and check to see if x and y are equal:

      // Assign a random number to x
      // Assign the cube root of the result of the cube function and x to y
      function doesXEqualY(x = getRandomNumber(), y = Math.cbrt(cube(x))) {
        return x === y
      }
      
      doesXEqualY()
      

      This will give the following:

      Output

      true

      A default parameter can even be a function definition, as seen in this example, which defines a parameter as the inner function and returns the function call of parameter:

      // Define a function with a default parameter that is an anonymous function
      function outer(
        parameter = function inner() {
          return 100
        }
      ) {
        return parameter()
      }
      
      // Invoke outer function
      outer()
      

      Output

      100

      This inner function will be created from scratch every time the outer function is invoked.

      Conclusion

      In this article, you learned what default function parameters are and how to use them. Now you can use default parameters to help keep your functions clean and easy to read. You can also assign empty objects and arrays to parameters upfront to reduce both complexity and lines of code when dealing with situations such as retrieving values from an object or looping through an array.

      If you would like to learn more about JavaScript, check out the homepage for our How To Code in JavaScript series, or browse our How to Code in Node.js series for articles on back-end development.



      Source link

      Understanding Generators in JavaScript


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      In ECMAScript 2015, generators were introduced to the JavaScript language. A generator is a process that can be paused and resumed and can yield multiple values. A generator in JavaScript consists of a generator function, which returns an iterable Generator object.

      Generators can maintain state, providing an efficient way to make iterators, and are capable of dealing with infinite data streams, which can be used to implement infinite scroll on the frontend of a web application, to operate on sound wave data, and more. Additionally, when used with Promises, generators can mimic the async/await functionality, which allows us to deal with asynchronous code in a more straightforward and readable manner. Although async/await is a more prevalent way to deal with common, simple asynchronous use cases, like fetching data from an API, generators have more advanced features that make learning how to use them worthwhile.

      In this article, we’ll cover how to create generator functions, how to iterate over Generator objects, the difference between yield and return inside a generator, and other aspects of working with generators.

      Generator Functions

      A generator function is a function that returns a Generator object, and is defined by the function keyword followed by an asterisk (*), as shown in the following:

      // Generator function declaration
      function* generatorFunction() {}
      

      Occasionally, you will see the asterisk next to the function name, as opposed to the function keyword, such as function *generatorFunction(). This works the same, but function* is a more widely accepted syntax.

      Generator functions can also be defined in an expression, like regular functions:

      // Generator function expression
      const generatorFunction = function*() {}
      

      Generators can even be the methods of an object or class:

      // Generator as the method of an object
      const generatorObj = {
        *generatorMethod() {},
      }
      
      // Generator as the method of a class
      class GeneratorClass {
        *generatorMethod() {}
      }
      

      The examples throughout this article will use the generator function declaration syntax.

      Note: Unlike regular functions, generators cannot be constructed with the new keyword, nor can they be used in conjunction with arrow functions.

      Now that you know how to declare generator functions, lets look at the iterable Generator objects that they return.

      Generator Objects

      Traditionally, functions in JavaScript run to completion, and calling a function will return a value when it arrives at the return keyword. If the return keyword is omitted, a function will implicitly return undefined.

      In the following code, for example, we declare a sum() function that returns a value that is the sum of two integer arguments:

      // A regular function that sums two values
      function sum(a, b) {
        return a + b
      }
      

      Calling the function returns a value that is the sum of the arguments:

      const value = sum(5, 6) // 11
      

      A generator function, however, does not return a value immediately, and instead returns an iterable Generator object. In the following example, we declare a function and give it a single return value, like a standard function:

      // Declare a generator function with a single return value
      function* generatorFunction() {
        return 'Hello, Generator!'
      }
      

      When we invoke the generator function, it will return the Generator object, which we can assign to a variable:

      // Assign the Generator object to generator
      const generator = generatorFunction()
      

      If this were a regular function, we would expect generator to give us the string returned in the function. However, what we actually get is an object in a suspended state. Calling generator will therefore give output similar to the following:

      Output

      generatorFunction {<suspended>} __proto__: Generator [[GeneratorLocation]]: VM272:1 [[GeneratorStatus]]: "suspended" [[GeneratorFunction]]: ƒ* generatorFunction() [[GeneratorReceiver]]: Window [[Scopes]]: Scopes[3]

      The Generator object returned by the function is an iterator. An iterator is an object that has a next() method available, which is used for iterating through a sequence of values. The next() method returns an object with value and done properties. value represent the returned value, and done indicates whether the iterator has run through all its values or not.

      Knowing this, let’s call next() on our generator and get the current value and state of the iterator:

      // Call the next method on the Generator object
      generator.next()
      

      This will give the following output:

      Output

      {value: "Hello, Generator!", done: true}

      The value returned from calling next() is Hello, Generator!, and the state of done is true, because this value came from a return that closed out the iterator. Since the iterator is done, the generator function’s status will change from suspended to closed. Calling generator again will give the following:

      Output

      generatorFunction {<closed>}

      As of right now, we’ve only demonstrated how a generator function can be a more complex way to get the return value of a function. But generator functions also have unique features that distinguish them from normal functions. In the next section, we’ll learn about the yield operator and see how a generator can pause and resume execution.

      yield Operators

      Generators introduce a new keyword to JavaScript: yield. yield can pause a generator function and return the value that follows yield, providing a lightweight way to iterate through values.

      In this example, we’ll pause the generator function three times with different values, and return a value at the end. Then we will assign our Generator object to the generator variable.

      // Create a generator function with multiple yields
      function* generatorFunction() {
        yield 'Neo'
        yield 'Morpheus'
        yield 'Trinity'
      
        return 'The Oracle'
      }
      
      const generator = generatorFunction()
      

      Now, when we call next() on the generator function, it will pause every time it encounters yield. done will be set to false after each yield, indicating that the generator has not finished. Once it encounters a return, or there are no more yields encountered in the function, done will flip to true, and the generator will be finished.

      Use the next() method four times in a row:

      // Call next four times
      generator.next()
      generator.next()
      generator.next()
      generator.next()
      

      These will give the following four lines of output in order:

      Output

      {value: "Neo", done: false} {value: "Morpheus", done: false} {value: "Trinity", done: false} {value: "The Oracle", done: true}

      Note that a generator does not require a return; if omitted, the last iteration will return {value: undefined, done: true}, as will any subsequent calls to next() after a generator has completed.

      Iterating Over a Generator

      Using the next() method, we manually iterated through the Generator object, receiving all the value and done properties of the full object. However, just like Array, Map, and Set, a Generator follows the iteration protocol, and can be iterated through with for...of:

      // Iterate over Generator object
      for (const value of generator) {
        console.log(value)
      }
      

      This will return the following:

      Output

      Neo Morpheus Trinity

      The spread operator can also be used to assign the values of a Generator to an array.

      // Create an array from the values of a Generator object
      const values = [...generator]
      
      console.log(values)
      

      This will give the following array:

      Output

      (3) ["Neo", "Morpheus", "Trinity"]

      Both spread and for...of will not factor the return into the values (in this case, it would have been 'The Oracle').

      Note: While both of these methods are effective for working with finite generators, if a generator is dealing with an infinite data stream, it won’t be possible to use spread or for...of directly without creating an infinite loop.

      Closing a Generator

      As we’ve seen, a generator can have its done property set to true and its status set to closed by iterating through all its values. There are two additional ways to immediately cancel a generator: with the return() method, and with the throw() method.

      With return(), the generator can be terminated at any point, just as if a return statement had been in the function body. You can pass an argument into return(), or leave it blank for an undefined value.

      To demonstrate return(), we’ll create a generator with a few yield values but no return in the function definition:

      function* generatorFunction() {
        yield 'Neo'
        yield 'Morpheus'
        yield 'Trinity'
      }
      
      const generator = generatorFunction()
      

      The first next() will give us 'Neo', with done set to false. If we invoke a return() method on the Generator object right after that, we’ll now get the passed value and done set to true. Any additional call to next() will give the default completed generator response with an undefined value.

      To demonstrate this, run the following three methods on generator:

      generator.next()
      generator.return('There is no spoon!')
      generator.next()
      

      This will give the three following results:

      Output

      {value: "Neo", done: false} {value: "There is no spoon!", done: true} {value: undefined, done: true}

      The return() method forced the Generator object to complete and to ignore any other yield keywords. This is particularly useful in asynchronous programming when you need to make functions cancelable, such as interrupting a web request when a user wants to perform a different action, as it is not possible to directly cancel a Promise.

      If the body of a generator function has a way to catch and deal with errors, you can use the throw() method to throw an error into the generator. This starts up the generator, throws the error in, and terminates the generator.

      To demonstrate this, we will put a try...catch inside the generator function body and log an error if one is found:

      // Define a generator function with a try...catch
      function* generatorFunction() {
        try {
          yield 'Neo'
          yield 'Morpheus'
        } catch (error) {
          console.log(error)
        }
      }
      
      // Invoke the generator and throw an error
      const generator = generatorFunction()
      

      Now, we will run the next() method, followed by throw():

      generator.next()
      generator.throw(new Error('Agent Smith!'))
      

      This will give the following output:

      Output

      {value: "Neo", done: false} Error: Agent Smith! {value: undefined, done: true}

      Using throw(), we injected an error into the generator, which was caught by the try...catch and logged to the console.

      Generator Object Methods and States

      The following table shows a list of methods that can be used on Generator objects:

      Method Description
      next() Returns the next value in a generator
      return() Returns a value in a generator and finishes the generator
      throw() Throws an error and finishes the generator

      The next table lists the possible states of a Generator object:

      Status Description
      suspended Generator has halted execution but has not terminated
      closed Generator has terminated by either encountering an error, returning, or iterating through all values

      yield Delegation

      In addition to the regular yield operator, generators can also use the yield* expression to delegate further values to another generator. When the yield* is encountered within a generator, it will go inside the delegated generator and begin iterating through all the yields until that generator is closed. This can be used to separate different generator functions to semantically organize your code, while still having all their yields be iterable in the right order.

      To demonstrate, we can create two generator functions, one of which will yield* operate on the other:

      // Generator function that will be delegated to
      function* delegate() {
        yield 3
        yield 4
      }
      
      // Outer generator function
      function* begin() {
        yield 1
        yield 2
        yield* delegate()
      }
      

      Next, let’s iterate through the begin() generator function:

      // Iterate through the outer generator
      const generator = begin()
      
      for (const value of generator) {
        console.log(value)
      }
      

      This will give the following values in the order they are generated:

      Output

      1 2 3 4

      The outer generator yielded the values 1 and 2, then delegated to the other generator with yield*, which returned 3 and 4.

      yield* can also delegate to any object that is iterable, such as an Array or a Map. Yield delegation can be helpful in organizing code, since any function within a generator that wanted to use yield would also have to be a generator.

      Infinite Data Streams

      One of the useful aspects of generators is the ability to work with infinite data streams and collections. This can be demonstrated by creating an infinite loop inside a generator function that increments a number by one.

      In the following code block, we define this generator function and then initiate the generator:

      // Define a generator function that increments by one
      function* incrementer() {
        let i = 0
      
        while (true) {
          yield i++
        }
      }
      
      // Initiate the generator
      const counter = incrementer()
      

      Now, iterate through the values using next():

      // Iterate through the values
      counter.next()
      counter.next()
      counter.next()
      counter.next()
      

      This will give the following output:

      Output

      {value: 0, done: false} {value: 1, done: false} {value: 2, done: false} {value: 3, done: false}

      The function returns successive values in the infinite loop while the done property remains false, ensuring that it will not finish.

      With generators, you don’t have to worry about creating an infinite loop, because you can halt and resume execution at will. However, you still have to have caution with how you invoke the generator. If you use spread or for...of on an infinite data stream, you will still be iterating over an infinite loop all at once, which will cause the environment to crash.

      For a more complex example of an infinite data stream, we can create a Fibonacci generator function. The Fibonacci sequence, which continuously adds the two previous values together, can be written using an infinite loop within a generator as follows:

      // Create a fibonacci generator function
      function* fibonacci() {
        let prev = 0
        let next = 1
      
        yield prev
        yield next
      
        // Add previous and next values and yield them forever
        while (true) {
          const newVal = next + prev
      
          yield newVal
      
          prev = next
          next = newVal
        }
      }
      

      To test this out, we can loop through a finite number and print the Fibonacci sequence to the console.

      // Print the first 10 values of fibonacci
      const fib = fibonacci()
      
      for (let i = 0; i < 10; i++) {
        console.log(fib.next().value)
      }
      

      This will give the following:

      Output

      0 1 1 2 3 5 8 13 21 34

      The ability to work with infinite data sets is one part of what makes generators so powerful. This can be useful for examples like implementing infinite scroll on the frontend of a web application.

      Passing Values in Generators

      Throughout this article, we’ve used generators as iterators, and we’ve yielded values in each iteration. In addition to producing values, generators can also consume values from next(). In this case, yield will contain a value.

      It’s important to note that the first next() that is called will not pass a value, but will only start the generator. To demonstrate this, we can log the value of yield and call next() a few times with some values.

      function* generatorFunction() {
        console.log(yield)
        console.log(yield)
      
        return 'The end'
      }
      
      const generator = generatorFunction()
      
      generator.next()
      generator.next(100)
      generator.next(200)
      

      This will give the following output:

      Output

      100 200 {value: "The end", done: true}

      It is also possible to seed the generator with an initial value. In the following example, we’ll make a for loop and pass each value into the next() method, but pass an argument to the initial function as well:

      function* generatorFunction(value) {
        while (true) {
          value = yield value * 10
        }
      }
      
      // Initiate a generator and seed it with an initial value
      const generator = generatorFunction(0)
      
      for (let i = 0; i < 5; i++) {
        console.log(generator.next(i).value)
      }
      

      We’ll retrieve the value from next() and yield a new value to the next iteration, which is the previous value times ten. This will give the following:

      Output

      0 10 20 30 40

      Another way to deal with starting up a generator is to wrap the generator in a function that will always call next() once before doing anything else.

      async/await with Generators

      An asynchronous function is a type of function available in ES6+ JavaScript that makes working with asynchronous data easier to understand by making it appear synchronous. Generators have a more extensive array of capabilities than asynchronous functions, but are capable of replicating similar behavior. Implementing asynchronous programming in this way can increase the flexibility of your code.

      In this section, we will demonstrate an example of reproducing async/await with generators.

      Let’s build an asynchronous function that uses the Fetch API to get data from the JSONPlaceholder API (which provides example JSON data for testing purposes) and logs the response to the console.

      Start out by defining an asynchronous function called getUsers that fetches data from the API and returns an array of objects, then call getUsers:

      const getUsers = async function() {
        const response = await fetch('https://jsonplaceholder.typicode.com/users')
        const json = await response.json()
      
        return json
      }
      
      // Call the getUsers function and log the response
      getUsers().then(response => console.log(response))
      

      This will give JSON data similar to the following:

      Output

      [ {id: 1, name: "Leanne Graham" ...}, {id: 2, name: "Ervin Howell" ...}, {id: 3, name": "Clementine Bauch" ...}, {id: 4, name: "Patricia Lebsack"...}, {id: 5, name: "Chelsey Dietrich"...}, ...]

      Using generators, we can create something almost identical that does not use the async/await keywords. Instead, it will use a new function we create and yield values instead of await promises.

      In the following code block, we define a function called getUsers that uses our new asyncAlt function (which we will write later on) to mimic async/await.

      const getUsers = asyncAlt(function*() {
        const response = yield fetch('https://jsonplaceholder.typicode.com/users')
        const json = yield response.json()
      
        return json
      })
      
      // Invoking the function
      getUsers().then(response => console.log(response))
      

      As we can see, it looks almost identical to the async/await implementation, except that there is a generator function being passed in that yields values.

      Now we can create an asyncAlt function that resembles an asynchronous function. asyncAlt has a generator function as a parameter, which is our function that yields the promises that fetch returns. asyncAlt returns a function itself, and resolves every promise it finds until the last one:

      // Define a function named asyncAlt that takes a generator function as an argument
      function asyncAlt(generatorFunction) {
        // Return a function
        return function() {
          // Create and assign the generator object
          const generator = generatorFunction()
      
          // Define a function that accepts the next iteration of the generator
          function resolve(next) {
            // If the generator is closed and there are no more values to yield,
            // resolve the last value
            if (next.done) {
              return Promise.resolve(next.value)
            }
      
            // If there are still values to yield, they are promises and
            // must be resolved.
            return Promise.resolve(next.value).then(response => {
              return resolve(generator.next(response))
            })
          }
      
          // Begin resolving promises
          return resolve(generator.next())
        }
      }
      

      This will give the same output as the async/await version:

      Output

      [ {id: 1, name: "Leanne Graham" ...}, {id: 2, name: "Ervin Howell" ...}, {id: 3, name": "Clementine Bauch" ...}, {id: 4, name: "Patricia Lebsack"...}, {id: 5, name: "Chelsey Dietrich"...}, ...]

      Note that this implementation is for demonstrating how generators can be used in place of async/await, and is not a production-ready design. It does not have error handling set up, nor does it have the ability to pass parameters into the yielded values. Though this method can add flexibility to your code, often async/await will be a better choice, since it abstracts implementation details away and lets you focus on writing productive code.

      Conclusion

      Generators are processes that can halt and resume execution. They are a powerful, versatile feature of JavaScript, although they are not commonly used. In this tutorial, we learned about generator functions and generator objects, methods available to generators, the yield and yield* operators, and generators used with finite and infinite data sets. We also explored one way to implement asynchronous code without nested callbacks or long promise chains.

      If you would like to learn more about JavaScript syntax, take a look at our Understanding This, Bind, Call, and Apply in JavaScript and Understanding Map and Set Objects in JavaScript tutorials.



      Source link