Lightning Web Component (LWC) - JavaScript

Array methods in JavaScript:

javascript methods doc

The Spread Operator (...)

  • The spread operator, denoted by three consecutive dots (...), is primarily used for expanding iterables like arrays into individual elements.

  • This operator allows us to efficiently merge, copy, or pass array elements to functions without explicitly iterating through them.

  • Spread Operator Use Cases

    1. Combining Arrays

  • The spread operator provides an elegant solution for combining multiple arrays into a single array. By spreading each array's elements within a new array, we can concatenate them effortlessly.
    const arr1 = [1, 2, 3];
    const arr2 = [4, 5, 6];
    const combined = [...arr1, ...arr2];
    console.log("Combined array:", combined); // [1, 2, 3, 4, 5, 6]
  • This approach eliminates the need for manual iteration or concatenation methods, resulting in concise and readable code.
  1. Passing Arguments to Functions

  • The spread operator simplifies the process of passing variable-length arguments to functions. Instead of specifying each argument individually, we can use the spread operator to unpack an array of values into function parameters.
    function sum(a, b, c) {
        return a + b + c;
    }

    const nums = [1, 2, 3];
    const result = sum(...nums);
    console.log("Result of sum:", result); // 6
  • This technique enhances function flexibility and reduces redundancy, especially when dealing with dynamic inputs.
  1. Copying Arrays

  • The spread operator offers a concise method for copying arrays, ensuring that modifications to the copied array do not affect the original. By spreading the original array's elements into a new array, we create a distinct copy.
    const original = [1, 2, 3];
    const copy = [...original];
    console.log("Copied array:", copy); // [1, 2, 3]
  • Unlike traditional methods like slice() or concat(), the spread operator provides a more intuitive and readable approach to array duplication.

default export:

promises:

callback hell:

Let’s understand what callbacks are and what problem related to callbacks is solved by promises.

Let's say we have a list of posts and their respective comments, like this:

const posts = [
  { post_id: 1, post_title: 'First Post' },
  { post_id: 2, post_title: 'Second Post' },
  { post_id: 3, post_title: 'Third Post' },
];

const comments = [
  { post_id: 2, comment: 'Great!'},
  { post_id: 2, comment: 'Nice Post!'},
  { post_id: 3, comment: 'Awesome Post!'},
];

Now, we will write a function to get a post by passing the post id. If the post is found, we will retrieve the comments related to that post.

const getPost = (id, callback) => {
 const post = posts.find( post => post.post_id === id);
 if(post) {
   callback(null, post);
 } else {
   callback("No such post found", undefined);
 }
};

const getComments = (post_id, callback) => {
 const result = comments.filter( comment => comment.post_id === post_id);
 if(result) {
   callback(null, result);
 } else {
   callback("No comments found", undefined);
 }
}

In the above getPost and getComments functions, if there is an error we will pass it as the first argument. But if we get the result, we will call the callback function and pass the result as the second argument to it.

If you are familiar with Node.js, then you will know that this is a very common pattern used in every Node.js callback function.

Now let’s use those functions:

getPost(2, (error, post) => {
    if(error) {
     return console.log(error);
    }
    console.log('Post:', post);
    getComments(post.post_id, (error, comments) => {
        if(error) {
          return console.log(error);
        }
        console.log('Comments:', comments);
    });
});

As you can see, we have the getComments function nested inside the getPost callback.

Now imagine if we also wanted to find the likes of those comments. That would also get nested inside getComments callback, creating more nesting. This will eventually make the code difficult to understand.

This nesting of the callbacks is known as callback hell.

You can see that the error handling condition also gets repeated in the code, which creates duplicate code – this is not good.

Asynch/Await:

In this section, we'll explore everything you need to know about async/await.

Async/await gives developers a better way to use promises.

To use async/await, you need to create a function and add the async keyword before the function name using ES5 function declaration syntax like this:

async function someFunction() {
  // function body
}

or using function expression syntax like this:

const someFunction = async function () {
  // function body
};

or using an arrow function like this:

const someFunction = async () => {
  // function body
};

Always remember that, when you add the async keyword to the function, it always returns a promise.

Take a look at the below code:

const sayHello = async function () {
  return 'Hello';
};

sayHello();

What do you think the output of the above code will be?

async_1

Result of calling function marked as async

The output is a promise fulfilled with the string Hello.

So the below code:

const sayHello = async function () {
  return 'Hello';
};

is the same as this:

const sayHello = function() {
 return new Promise((resolve, reject) => {
  resolve('Hello');
 });
}

which is the same as this:

const sayHello = function () {
  return Promise.resolve('Hello');
};

Promise.resolve('Hello') is just a shorter way of creating a promise which resolves to the string Hello.

So to get the actual string Hello, we need to add the .then handler like this:

sayHello().then(function (result) {
  console.log(result); // Hello
});

async_hello

Getting result of async function using .then handler

Now, where do we use the await keyword?

It's used inside the function which is declared as async. So the await keyword should only be used inside the async function.

You will get an error if you try to use it in non-async functions.

Suppose, we have a promise which returns the product of two numbers like this:

function getProduct(a, b) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(a * b);
    }, 1000);
  });
}

and we're using it like this:

getProduct(2, 4)
  .then(function (result) {
    getProduct(result, 2)
      .then(function (finalResult) {
        console.log('final_result', finalResult);
      })
      .catch(function (error) {
        console.log(error);
      });
  })
  .catch(function (error) {
    console.log(error);
  });

In the above code, we're first getting the product of 2 and 4. Then we're using that result to multiply it by 2 again, and then finally printing the product.

If you execute the above code, you will see the final result as 16 which is 2 4 = 8 and 8 2 = 16.

async_product

Result of nesting .then callback handlers

The above code of .then and .catch looks pretty complicated and difficult to understand at one glance.

So using async/await we can simplify the above code to this:

const printResult = async () => {
  try {
    const result = await getProduct(2, 4); // line 1
    const finalResult = await getProduct(result, 2); // line 2
    console.log('final_result', finalResult); // line 3
  } catch (error) {
    console.log(error);
  }
};

printResult();

This looks much cleaner and easy to understand.

Here, to use the await keyword, we're declaring a function with the async keyword. Then to get the result of each promise, we're adding the await keyword in front of it.

Also, note that we've added try/catch inside the function. You always need to add a try block around the code which uses await so the catch block will be executed if the promise gets rejected.

There is a very important thing you need to remember: The above async/await code will work exactly the same as when we use .then – so the next await line (line 2) will not be executed until the previous await call (line 1) is successful.

Therefore, as the getProduct function is taking 1 second to execute because of the setTimeout call, line 2 will have to wait for 1 second before executing the getProduct function again.

But there is one exception to this behavior, which you can check out in this article.

Also, if there is an error while executing line 1 (because of some error that occurred in the getProduct function), the next code after line 1 will not be executed. Instead, the catch block will be executed.

Now, if you compare the code of promise chaining and async/await, you will see the difference.

// code using async/await

const printResult = async () => {
  try {
    const product = await getProduct(2, 4); // line 1
    const finalResult = await getProduct(product, 2); // line 2
    console.log('final_result', finalResult); // line 3
  } catch (error) {
    console.log(error);
  }
};

printResult();
// code using .then and .catch

getProduct(2, 4)
  .then(function (result) {
    getProduct(result, 2)
      .then(function (finalResult) {
        console.log('final_result', finalResult);
      })
      .catch(function (error) {
        console.log(error);
      });
  })
  .catch(function (error) {
    console.log(error);
  });

As you can see, the code using async/await is much cleaner and easy to understand as compared to the promise chaining.

As the nesting gets deeper, the code using promise chaining gets more complicated. So async/await just provides a way to write the same code but with better clarity.

Using async/await also reduces the need of adding multiple .catch handlers to handle the errors.

We can avoid the nesting in the above promise chaining by writing the previous code like this:

getProduct(2, 4)
  .then(function (result) {
    return getProduct(result, 2);
  })
  .then(function (finalResult) {
    console.log('final_result', finalResult);
  })
  .catch(function (error) {
    console.log(error);
  });

Here, from the first .then handler, we're returning the result of getProduct(result, 2).

Whatever returned from the previous .then handler will be passed to the next .then handler.

As the getProduct function returns a promise so we can attach .then again to it and avoid the need for a nested .catch handler.

avoid_nested_handlers

using promise chaining

But still async/await syntax looks cleaner and easier to understand than the promise chaining syntax.

Functions in JS:

there were two main ways of declaring functions.

1. Function Declaration Syntax:

function add(a, b) {
 return a + b;
}

2. Function Expression Syntax:

const add = function(a, b) {
 return a + b;
};

The main visible difference between the regular function and arrow function is the syntax of writing the function.

Using arrow function syntax, we can write the above adding function like this:

const add = (a, b) => {
 return a + b;
};

You might not see much difference here, apart from the arrow. But if we have a single line of code in the function body we can simplify the above arrow function like this:

const add = (a, b) => a + b;

Here we are implicitly returning the result of a + b, so there is no need for a return keyword if there is a single statement.

So using the arrow functions will make your code much shorter.

Implicit vs. explicit coercion

  • convert between types like Number(value), it’s called explicit type coercion (or type casting).

  • values can also be converted between different types automatically, and it is called implicit type coercion.

      String(123) // explicit
      123 + ''    // implicit
    

Three types of conversion

The first rule to know is there are only three types of conversion in JavaScript:

  1. to string

    • To explicitly convert String() function.

    • Implicit coercion is triggered by the binary + operator, when any operand is a string:

        String(123) // explicit
        123 + ''    // implicit
      

      All primitive values are converted to strings naturally as you might expect:

        String(123)                   // '123'
        String(-12.3)                 // '-12.3'
        String(null)                  // 'null'
        String(undefined)             // 'undefined'
        String(true)                  // 'true'
        String(false)                 // 'false'
      
  2. to boolean

    To explicitly convert apply Boolean() function.
    Impli
    cit conversion happens in logical context, or is triggered by logical operators ( ||&&!) .

     Boolean(2)          // explicit
     if (2) { ... }      // implicit due to logical context
     !!2                 // implicit due to logical operator
     2 || 'hello'        // implicit due to logical operator
    
     Boolean({})             // true
     Boolean([])             // true
     Boolean(Symbol())       // true
     !!Symbol()              // true
     Boolean(function() {})  // true
     Boolean('')           // false
     Boolean(0)            // false     
     Boolean(-0)           // false
     Boolean(NaN)          // false
     Boolean(null)         // false
     Boolean(undefined)    // false
    
  3. to number

    For an explicit conversion just apply the Number() function, same as you did with Boolean() and String() .

    Implicit conversion is tricky, because it’s triggered in more cases:

    • comparison operators (>, <, <=,>=)

    • bitwise operators ( |&^~)

    • arithmetic operators (-+*/% ). Note, that binary+ does not trigger numeric conversion, when any operand is a string.

    • unary + operator

    • loose equality operator == (incl. !=).
      Note that == does not trigger numeric conversion when both operands are strings.

    Number('123')   // explicit
    +'123'          // implicit
    123 != '456'    // implicit
    4 > '5'         // implicit
    5/null          // implicit
    true | 0        // implicit

How is == Different from === in JavaScript?

double equals operator (== ):

Datatypes of the operands we are comparing are different, then the JavaScript Engine automatically converts one of the operands to be the same as the other one in order to make the comparison possible.

Example:

const a = 100;
const b = '100';

console.log(a == b) // true

It is important to note that the actual values remains unchanged. It only implicitly gets converted while comparing.

Rules for conversion

  • If either operand is a string, the other operand will be converted to a string.

  • If either operand is a number, the other operand will be converted to a number.

  • If either operand is a boolean, it will be converted to a number (true becomes 1 and false becomes 0).

  • If one operand is an object and the other is a primitive value, the object will be converted to a primitive value before the comparison is made.

  • If one of the operands is null or undefined, the other must also be null or undefined to return true. Otherwise it will return false.

Triple Equals (===)

  • Triple equals (===), also referred to as "strict equality", works similarly to how double equals (==) works, with one important difference: it does not convert the types of the operands before comparing.

  • While comparing the variables, it first checks if the types differ. If they do, it returns false. If the types match, then it checks for the value. If the values are same and are not numbers, it returns true.

  • Finally, if both the operands are numbers and are not NaN, and they have the same value, then it returns true. Otherwise, false.