ES6 Features Part-2

amansingh

Aman Singh

Posted on February 25, 2023

ES6 Features Part-2

Class

  • A class is a blueprint or a template for creating objects that have similar properties and methods. It is a way to create a user-defined data type that encapsulates data and functionality.

  • Classes provide a way to create objects that have certain properties and methods

  • Here's an example of how to define a class in JavaScript:

    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    
      sayHello() {
        console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
      }
    }
    
  • To create an instance of the Person class, we can use the new keyword:

    const john = new Person('John', 30);
    john.sayHello(); // logs "Hello, my name is John and I am 30 years old."
    

Spread & Rest Operator

  • It provide a concise and flexible way to work with arrays, objects, and function arguments.

  • The spread operator, represented by three dots (...), is used to "spread" the elements of an array or an iterable object (such as a string, set, or map) into a new array, function arguments, or object literals.

    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];
    const concatenated = [...arr1, ...arr2];
    console.log(concatenated); // [1, 2, 3, 4, 5, 6]
    
  • The spread operator can also be used to create a shallow copy of an array or object.

  • The spread operator can be used in combination with other elements to create new arrays or objects.

  • The spread operator can be used to convert a string to an array of characters.

  • The rest operator, also represented by three dots, is used in function definitions to capture a variable number of arguments into an array.

    function sum(...numbers) {
      return numbers.reduce((acc, curr) => acc + curr, 0);
    }
    
    console.log(sum(1, 2, 3, 4)); // 10
    console.log(sum(1)); // 1
    console.log(sum()); // 0
    
  • The rest operator can be used to define functions with a variable number of arguments, which is also called a "varargs" function.

  • The rest operator can be used with destructuring to capture all remaining elements in an array.

  • The rest operator can be used with default parameter values to create functions with optional parameters.

    Note: The spread and rest operators are similar in syntax and usage, but they have different effects depending on where they are used. The spread operator "expands" an array or iterable object, while the rest operator "collects" a variable number of arguments or array elements into an array.

Promises

  • A promise is an object that represents a value that may not yet be available but is expected to be resolved at some point in the future. The resolution can be a success or a failure.

  • A promise has three possible states:

    • Pending: The initial state. The promise is neither resolved nor rejected.
    • Fulfilled: The state when the promise is resolved successfully. This means that the desired value is available.
    • Rejected: The state when the promise is rejected. This means that an error occurred while trying to resolve the promise.
  • The syntax for creating a promise is as follows:

    const promise = new Promise((resolve, reject) => {
      // Do some asynchronous operation, then either:
      // resolve(result) if successful, or
      // reject(error) if there is an error
    });
    

    The new Promise() constructor takes a function that receives two functions as arguments: resolve and reject. These functions are used to resolve or reject the promise with a value or an error, respectively.

    Once a promise is created, you can attach handlers to it using the .then() and .catch() methods. The .then() method is called when the promise is resolved successfully, and the .catch() method is called when the promise is rejected.

  • For example:

    promise.then(result => {
      // Do something with the result
    }).catch(error => {
      // Handle the error
    });
    
  • promise.then(result => { // Do something with the result }).catch(error => { // Handle the error });

    promise.then(result1 => {
      // Do something with result1
      return anotherPromise;
    }).then(result2 => {
      // Do something with result2
    }).catch(error => {
      // Handle any errors that occur
    });
    
  • Promise.all(): It is a method that can be used to wait for all the promises in an array to resolve or reject. It takes an array of promises as input and returns a new promise that is resolved with an array of results when all the promises in the input array are resolved. If any of the promises in the input array is rejected, the returned promise will be rejected with the reason of the first rejected promise.

    const promise1 = Promise.resolve(1);
    const promise2 = Promise.resolve(2);
    const promise3 = Promise.resolve(3);
    
    Promise.all([promise1, promise2, promise3])
      .then(results => {
        console.log(results); // [1, 2, 3]
      })
      .catch(error => {
        console.error(error);
      });
    
  • Promise.race(): It is a method that can be used to wait for the first promise in an array to resolve or reject. It takes an array of promises as input and returns a new promise that is resolved with the value of the first resolved promise, or rejected with the reason of the first rejected promise.

    const promise1 = new Promise(resolve => setTimeout(resolve, 1000, 'foo'));
    const promise2 = new Promise(resolve => setTimeout(resolve, 2000, 'bar'));
    
    Promise.race([promise1, promise2])
      .then(result => {
        console.log(result); // 'foo' (because promise1 resolves first)
      })
      .catch(error => {
        console.error(error);
      });
    
  • Promise.resolve() and Promise.reject()

    • Promise.resolve() is a method that can be used to create a resolved promise with a given value.

      const promise = Promise.resolve('foo');
      
      promise.then(result => {
        console.log(result); // 'foo'
      });
      
    • Promise.reject() is a method that can be used to create a rejected promise with a given reason.

      const promise = Promise.reject(new Error('Something went wrong'));
      
      promise.catch(error => {
        console.error(error); // 'Error: Something went wrong'
      });
      
  • Promise composition Promise composition is the act of creating a new promise that depends on the outcome of one or more other promises. This can be done using the .then() method, which returns a new promise that can be used to create more complex asynchronous workflows.

    function getUser(userId) {
      return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
        .then(response => response.json());
    }
    
    function getUserPosts(user) {
      return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`)
        .then(response => response.json());
    }
    
    getUser(1)
      .then(user => getUserPosts(user))
      .then(posts => console.log(posts))
      .catch(error => console.error(error));
    

    In this example, we define two functions: getUser() and getUserPosts(). The getUser() function makes a request to the JSONPlaceholder API to fetch a user by ID. It returns a promise that resolves with the parsed JSON data from the response. The getUserPosts() function takes a user object as input and makes a request to the JSONPlaceholder API to fetch all the posts by that user. It also returns a promise that resolves with the parsed JSON data from the response.

    We then use the two functions in a promise chain to create a new promise that depends on the outcome of the two original promises. We use the first .then() method to pass the user object to the getUserPosts() function, which returns a promise that resolves with the posts by that user. We then use the second .then() method to handle the posts data. The .catch() method is used to handle any errors that occur.

Promise anti-patterns

  • While promises can simplify the management of asynchronous operations, there are some anti-patterns that can lead to less readable and less maintainable code.

  • Using promises just for the sake of using them, even when callbacks or other techniques would be more appropriate.

  • Chaining too many promises together, which can make the code harder to read and debug.

  • Mixing promises and callbacks, which can lead to confusion and errors.

  • Not handling errors properly, which can lead to uncaught exceptions and other issues.

Downside of Promises

  • Promises are not cancellable: Once a Promise is created, it cannot be cancelled. This means that if you create a Promise that takes a long time to resolve or reject, there is no way to stop it. This can be a problem if you create too many Promises and they all end up taking a long time to resolve or reject, as this can result in a bottleneck in your application.

  • Promises can be more difficult to debug: Because Promises involve multiple layers of abstraction, they can be more difficult to debug than simple callbacks.

  • Promises can cause performance issues: While Promises are intended to improve performance by allowing asynchronous operations to run in the background, they can actually cause performance issues if they are overused. Creating too many Promises can cause a backlog of work that can slow down the application, especially on mobile devices or low-powered computers.

  • Promises can be difficult to understand: Although Promises are intended to make asynchronous code easier to write, they can be difficult to understand for developers who are new to JavaScript or have not used Promises before. The syntax of Promises can be confusing, and understanding how they work requires a good understanding of asynchronous programming.

Async & await

  • An async function is a function that always returns a Promise. The async keyword is used to indicate that the function contains asynchronous code.

  • A best alternative of Promise.

    async function getUserName(userId) {
      const user = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
      const userData = await user.json();
      return userData.name;
    }
    
  • The await keyword is used inside an async function to pause the execution of the function until a Promise resolves.

    async function getUserPosts(userId) {
      const user = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
      const userData = await user.json();
      const posts = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
      const postData = await posts.json();
      return { user: userData, posts: postData };
    }
    
  • Chaining Promises async and await can be used in combination with Promises to create complex asynchronous operations.

    async function getUsersAndPosts() {
      const [users, posts] = await Promise.all([
        fetch('https://jsonplaceholder.typicode.com/users'),
        fetch('https://jsonplaceholder.typicode.com/posts')
      ]);
    
      const userData = await users.json();
      const postData = await posts.json();
    
      return { users: userData, posts: postData };
    }
    
  • async and await can make asynchronous code easier to read and write by allowing you to write asynchronous code in a synchronous style.

  • async and await can simplify complex asynchronous operations by allowing you to chain Promises and wait for them to resolve before continuing.

Modules

  • module is a reusable piece of code that can be shared between different parts of an application.

  • Modules can help keep code organized, reduce duplication, and make it easier to manage dependencies.

  • There are two main types of modules in JavaScript:

    • CommonJS: Ir is a module format that is used in Node.js, and it uses the require and module.exports keywords to import and export modules, respectively.

      // math.js
      function add(a, b) {
        return a + b;
      }
      
      function subtract(a, b) {
        return a - b;
      }
      
      module.exports = {
        add,
        subtract
      };
      

      Other parts of the application can import these functions using the require keyword:

    // index.js
    const math = require('./math');
    
    console.log(math.add(1, 2)); // 3
    console.log(math.subtract(5, 3)); // 2
    
  • ES6 Modules: ES6 modules are a newer module format that is built into modern browsers and supported by newer versions of Node.js. ES6 modules use the import and export keywords to import and export modules.

    // math.js
    export function add(a, b) {
      return a + b;
    }
    
    export function subtract(a, b) {
      return a - b;
    }
    

    Other parts of the application can import these functions using the import keyword:

    // index.js
    import { add, subtract } from './math.js';
    
    console.log(add(1, 2)); // 3
    console.log(subtract(5, 3)); // 2
    
  • Modules allow for better code organization: By breaking up your code into smaller, more manageable pieces, you can make your code easier to read and maintain.

  • Modules can help with code reusability.

  • Modules can help with dependency management.

  • Modules can improve performance.

  • Modules can be asynchronous.

Map

  • Map is a built-in object that allows you to store key-value pairs, where both the keys and the values can be of any type, including objects and functions.

  • Maps are similar to JavaScript objects, but they offer some additional features and functionality that make them useful for certain use cases.

  • Some key features and benefits of Map objects in JavaScript:

    • Custom keys: Unlike JavaScript objects, which can only use strings or symbols as keys, Map objects allow you to use any value as a key, including objects, functions, and even other maps.
    • Iteration: Map objects have built-in iteration methods, such as Map.prototype.forEach() and Map.prototype.entries(), which allow you to iterate over the key-value pairs in the map in a predictable and consistent order.
    • Size tracking: Map objects have a built-in size property that allows you to easily determine the number of key-value pairs in the map.
    • Efficient querying: Because Map objects use a hash table under the hood, looking up a value by key is generally more efficient than doing so with a JavaScript object, especially for larger maps.
    • Key and value equality: Unlike JavaScript objects, which compare keys based on their reference identity, Map objects use a strict equality comparison to determine key equality. This can be useful when dealing with complex objects or values that may not have unique reference identities.
    • Map objects have a delete() method to remove key-value pairs, and a clear() method to remove all key-value pairs.

      const myMap = new Map();
      
      // Set key-value pairs
      myMap.set('name', 'John');
      myMap.set(123, 'some value');
      myMap.set({ key: 'value' }, 'another value');
      
      // Get values by key
      console.log(myMap.get('name')); // 'John'
      console.log(myMap.get(123)); // 'some value'
      console.log(myMap.get({ key: 'value' })); // 'another value'
      
      // Iterate over the key-value pairs
      myMap.forEach((value, key) => {
        console.log(`${key}: ${value}`);
      });
      
      // Get the number of key-value pairs
      console.log(myMap.size); // 3
      
      // deleting key
      myMap.delete('name');
      
  • Map objects can be a useful tool in JavaScript for storing and querying key-value pairs, especially when dealing with complex or non-string keys.

  • Map objects are iterable: You can use a for...of loop to iterate over the key-value pairs in a Map, or you can use the entries(), keys(), or values() methods to get an iterator for the keys, values, or key-value pairs in the Map.

  • Setting values for the same key multiple times will override the previous value: Unlike JavaScript objects, which will allow you to set the same property multiple times without overwriting the previous value, Map objects will override the previous value for the same key.

    // Using a Map
    const myMap = new Map();
    
    myMap.set('key1', 'value1');
    myMap.set('key2', 'value2');
    myMap.set('key1', 'new value1');
    
    console.log('Map:');
    for (const [key, value] of myMap) {
      console.log(`${key} -> ${value}`);
    }
    
    // Using a JavaScript object
    const myObj = {};
    
    myObj.prop1 = 'value1';
    myObj.prop2 = 'value2';
    myObj.prop1 = 'new value1';
    
    console.log('Object:');
    for (const key in myObj) {
      if (myObj.hasOwnProperty(key)) {
        console.log(`${key} -> ${myObj[key]}`);
      }
    }
    

    In the Map example, when we set the value for the key 'key1' a second time, it overwrites the previous value with the new value. This means that when we iterate over the Map, the key 'key1' only appears once, and its value is 'new value1'.

    In the JavaScript object example, when we set the value for the property 'prop1' a second time, it adds a new property with the same name, rather than overwriting the previous value. This means that when we iterate over the object, the key 'prop1' appears twice, with the values 'value1' and 'new value1', respectively.

NOTE: The key difference between using a Map and a JavaScript object: a Map allows us to store unique keys and their values, while a JavaScript object allows us to store properties with the same name, but different values.

If you enjoyed this article and would like to read more, Be sure to Follow me for regular updates.

💖 💪 🙅 🚩
amansingh
Aman Singh

Posted on February 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

ES6 Features Part-2
es6 ES6 Features Part-2

February 25, 2023

Quick Guide to Destructuring in ES6
javascript Quick Guide to Destructuring in ES6

July 21, 2020