JavaScript Functions: a Comprehensive guide

JavaScript functions are an essential part of the JavaScript programming language. They are reusable blocks of code that perform specific tasks or calculations. Functions can take in parameters, perform operations, and return values. In this comprehensive guide, we will explore the various aspects of JavaScript functions, including function declarations, function expressions, arrow functions, anonymous functions, higher-order functions, and more.

Function Declarations:

A function declaration defines a named function using the “function” keyword. It consists of the function name, a list of parameters (optional), and the function body enclosed in curly braces.

function walk(road) {
  console.log("Walking on the " + road + " road!");
}
walk("Chevy Chase Circle"); // Output: Walking on the Chevy Chase Circle road!

Function Expressions:

Function expressions define functions as values assigned to variables. They don’t require a function name (anonymous functions) or can have a name for better debugging (named function expressions).

//anonymous function assigned to variable walk
var walk = function(road) {
  console.log("Walking around " + road + "!");
};
walk("Dupont Circle"); // Output: Walking around Dupont Circle!

Arrow Functions:

Arrow functions are a concise syntax introduced in ES6 for writing functions. They have a more compact syntax compared to regular functions. Unlike regular functions, arrow functions do not bind their own this value. Instead, they lexically bind this to the enclosing scope. This behavior is often desired when working with callbacks and event handlers.


var walk = road => {
  console.log("Walking around " + road + "!");
};
walk("Grant Circle"); // Output: Walking around Grant Circle!
var road = {
  name: "Logan Circle",
  walk: function() {
    setTimeout(() => {
      console.log("Walking around " + this.name + "!");
    }, 1000);
  }
};
road.walk(); // Output: Walking around Logan Circle! (after 1 second)

road.walk(); 
road.name = "Scott Circle";
road.walk();
/* output:
Walking around Scott Circle!
Walking around Scott Circle!
*/

In the above, can you guess why it’s giving the output “Walking around Scott Circle! Walking around Scott Circle!” while the expected output was “Walking around Logan Circle! Walking around Scott Circle!”?

road.walk() is called for the first time. Inside the walk method, the setTimeout function is called with an arrow function as its callback. The setTimeout function schedules the arrow function to be executed after a delay of 1000 milliseconds (1 second). At this point, the this inside the arrow function refers to the this from the walk method, which is the road object. However, the code executes asynchronously, so by the time the arrow function executes, the value of road.name is already updated to “Scott Circle” When the arrow function executes after 1 second, it logs “Walking around Scott Circle!” because this.name refers to road.name, which is “Scott Circle” (the updated value). road.name was updated to “Scott Circle” before the first call to road.walk(), so it never logs “Walking around Logan Circle!”

Arrow function syntax:

Below I tried to list all possible scenarios.

  1. Arrow function with parentheses for a single parameter:
(parameter) => { /* function body */ }
  1. Arrow function without parentheses for a single parameter:
parameter => { /* function body */ }
  1. Arrow function with parentheses for multiple parameters:
(parameter1, parameter2) => { /* function body */ }
  1. Arrow function with parentheses for zero parameters (empty parentheses):
() => { /* function body */ }
  1. Arrow function with concise syntax for a single expression (implicit return):
(parameter) => expression
  1. Arrow function with concise syntax for a single expression and without parentheses for a single parameter:
parameter => expression
  1. Arrow function with object literal return (requires parentheses around the object literal):
(parameter) => ({ key: value })
  1. Arrow function as an IIFE (Immediately Invoked Function Expression):
((parameter) => { /* function body */ })(argument);
  1. Arrow function as a callback function:
array.map(parameter => { /* function body */ });

Arrow functions provide a concise syntax for writing functions. They have a few differences from regular functions, such as lexical scoping of this, lack of a arguments object, and the inability to be used as constructors with new. Arrow functions are especially useful for short, single-expression functions and when you need to retain the lexical scope of this.

Function Parameters and Arguments:

Functions can accept parameters, which are variables listed in the function declaration. Arguments are the actual values passed into the function when it’s invoked.

function cells(row, column) {
  return row * column;
} //parameters row and column
var numberOfCells = cells(5, 3); // Arguments: 5 and 3
console.log(numberOfCells); // Output: 15

Return Statement:

The return statement is used to specify the value that a function should return. It immediately terminates the function execution and sends the value back to the calling code.

function totalPrice(tomatoPrice, potatoPrice) {
  return tomatoPrice + potatoPrice;
  console.log("this won't be logged in the console");
}
var expense = totalPrice(2, 3);
console.log(expense); // Output: 5

Anonymous Functions:

Anonymous functions are functions without a name. They can be used as arguments to other functions or assigned to variables.

An anonymous function in JavaScript is a function that doesn’t have a specified name. It is created without any identifier and is typically used in situations where the function is used only once or as a callback function. Anonymous functions can be defined using function expressions or as arguments to other functions.

Function Expression Example:

var totalCost = function(costOne, costTwo) {
  return costOne + costTwo;
};
console.log(totalCost(2, 3)); // Output: 5

In this example, we define an anonymous function using a function expression and assign it to the variable totalCost. The function takes two parameters costOne and costTwo, and it returns their sum.

Anonymous functions are also commonly used as callback functions for event handling, timers, asynchronous operations, array methods, etc. Here’s an example of an anonymous function used as a callback for the setTimeout function:

setTimeout(function() {
  console.log("This is an anonymous function used as a callback.");
}, 1000);

In this example, the setTimeout function takes an anonymous function as its first argument, which will be executed after a delay of 1000 milliseconds (1 second).

Anonymous functions are powerful because they allow us to create functions on the fly without explicitly defining a name for them. They are useful when you need a function for a specific purpose and don’t intend to reuse it elsewhere in your code. However, in cases where you want to reuse the function or have a more organized codebase, named functions are often preferred.

Higher Order Functions:

Higher-order functions are functions that can accept other functions as arguments or return functions as results. They enable powerful functional programming paradigms in JavaScript.

function sayHello() {
  return function() {
    console.log("Hello!");
  };
}
var hello = sayHello();
hello(); // Output: Hello!

Function Scope and Closures:

JavaScript functions have their own scope, meaning variables defined inside a function are not accessible outside. Closures allow inner functions to access variables from their outer (enclosing) function even after the outer function has returned.

function outer() {
  var name = "Sam";
  function inner() {
    console.log("Hello, " + name + "!");
  }
  return inner;
}
var greet = outer();
greet(); // Output: Hello, Sam!

You can read more about function scope and closures in JavaScript here.

Immediately Invoked Function Expressions (IIFE):

An Immediately Invoked Function Expression is a self-invoking function that is executed immediately after it’s defined. It is typically used to create a private scope and avoid polluting the global namespace.

(function() {
  console.log("This function is immediately invoked.");
})();
//This function is immediately invoked.

Default Parameters:

JavaScript allows you to specify default values for function parameters. If an argument is not provided when invoking the function, the default value will be used instead.

function greet(name = "Guest") {
  console.log("Hello, " + name + "!");
}
greet(); // Output: Hello, Guest!
greet("Sam"); // Output: Hello, Sam!

Here name has a default value “Guest” if function is called without passing an argument the default value is used.

Rest Parameters:

Rest parameters allow you to pass an indefinite number of arguments to a function as an array. They are indicated by the ellipsis (...) followed by a parameter name.

function sum(...numbers) {
  console.log(numbers);
  return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // Output: 6
console.log(sum(4, 5, 6, 7)); // Output: 22
/*
[ 1, 2, 3 ]
6
[ 4, 5, 6, 7 ]
22
*/

Here numbers is an array of arguments passed to the function , in sum(1, 2, 3) , numbers will be [1, 2, 3] and in sum(4, 5, 6, 7) , numbers will be [4, 5, 6, 7]

Function Hoisting:

Function hoisting is a behavior in JavaScript where function declarations are moved to the top of their containing scope during the creation phase of the execution context. This means that you can call a function before it is defined in the code, and it will still work due to hoisting. However, it’s essential to understand that only function declarations are hoisted, not function expressions.

Let’s illustrate function hoisting with an example:

sayHello(); // Output: "Hello, there!"
function sayHello() {
  console.log("Hello, there!");
}

In this example, we call the sayHello() function before its declaration. Despite that, the code works fine because the function declaration is hoisted to the top of its scope during the creation phase, and the function is available for execution.

However function hoisting does not apply for below code:

sayHi(); // TypeError: sayHi is not a function
var sayHi = function() {
  console.log("Hi!");
};

In this case, we are using a function expression to assign an anonymous function to the variable sayHi. Function expressions are not hoisted like function declarations. During the creation phase, the variable sayHi is hoisted and initialized to undefined, but the assignment of the function only happens during the execution phase. Therefore, when we try to call sayHi() before the assignment, we get a TypeError since sayHi is not yet a function.

Arrow functions are not hoisted in the same way as regular function declarations. Hoisting applies only to function declarations, not to function expressions, and arrow functions are a type of function expression.

Arrow functions are not hoisted:

sayHello(); // TypeError: sayHello is not a function
var sayHello = () => {
  console.log("Hello!");
};

In this example, we are using an arrow function expression to assign a function to the variable sayHello. When we try to call sayHello() before the assignment, we get a TypeError since sayHello is not yet a function. Arrow functions are not hoisted like function declarations. When using arrow functions, you should define them before you use them to avoid any unexpected behavior or errors.

function hoisting applies only to function declarations, not to function expressions. Function declarations are hoisted to the top of their scope, making them available for use anywhere in that scope, even before their actual declaration in the code. On the other hand, function expressions are not hoisted, and you should define them before you use them to avoid any unexpected behavior or errors.

Recursion:

Recursion is a programming technique where a function calls itself repeatedly until it reaches a specified base condition. It allows for solving complex problems by breaking them down into smaller, more manageable subproblems. Recursive functions can be powerful and elegant, but they require careful design to ensure they terminate correctly.

Let’s explore recursion with examples to illustrate its usage and mechanics.

Factorial Calculation using Recursion:

function factorial(number) {
  if (number === 0 || number === 1) {
    return 1; // Base condition: factorial of 0 or 1 is 1
  } else {
    return number * factorial(number - 1); // Recursive call
  }
}
console.log(factorial(5)); // Output: 120

In this example, the factorial() function derives the factorial of a number using recursion. The base condition checks if number is 0 or 1 and returns 1. Otherwise, it makes a recursive call to factorial(number - 1) and multiplies it by number. This process continues until the base condition is reached.

Fibonacci Sequence:

function fibonacci(n) {
  if (n === 0) {
    return 0; // Base condition: fibonacci(0) is 0
  } else if (n === 1) {
    return 1; // Base condition: fibonacci(1) is 1
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2); // Recursive call
  }
}
console.log(fibonacci(7)); // Output: 13

In this example, the fibonacci() function calculates the nth number in the Fibonacci sequence using recursion. The base conditions are set for n equal to 0 and 1, returning 0 and 1, respectively. For any other value of n, the function makes two recursive calls to calculate the previous two Fibonacci numbers and sums them up.Note that this code is not optimized. We can use techniques like memoization to optimize this code.

Directory Traversal (File System):

function traverseDirectory(directory) {
  console.log(directory.name);
  if (directory.type === 'folder') {
    for (var i = 0; i < directory.children.length; i++) {
      traverseDirectory(directory.children[i]); // Recursive call
    }
  }
}
var root = {
  name: 'Root',
  type: 'folder',
  children: [
    {
      name: 'Folder A',
      type: 'folder',
      children: [
        {
          name: 'File A1',
          type: 'file'
        },
        {
          name: 'File A2',
          type: 'file'
        }
      ]
    },
    {
      name: 'File B',
      type: 'file'
    }
  ]
};
traverseDirectory(root);
/* 
Root
Folder A
File A1
File A2
File B
*/

In this example, the traverseDirectory() function recursively traverses a directory structure and prints the names of all files and folders. If the current item is a folder, it makes a recursive call for each child within that folder, continuing the traversal.

Recursion can be a powerful technique for solving problems that exhibit repetitive substructures. However, it’s crucial to consider the base case to ensure the recursive calls eventually reach a stopping condition and prevent infinite recursion. Careful design and understanding of the problem at hand are essential for implementing recursion correctly and efficiently.

Function Methods:

function.prototype.method:

In JavaScript, function.prototype.method refers to adding a new method to the prototype object of a constructor function. It is a way to extend the functionality of all instances created by that constructor function.

In JavaScript, functions are objects, and every function has a property called prototype. When you create a constructor function using the function keyword, you can add methods and properties to its prototype, and they will be shared by all instances created by that constructor.

adding a method to the prototype of a constructor function:

// Constructor function
  function School(schoolName, establishedYear) {
    this.schoolName = schoolName;
    this.establishedYear = establishedYear;
  }
  // Adding a method to the prototype of the School constructor
  School.prototype.schoolInfo = function() {
    console.log(`The school ${this.schoolName} established in ${this.establishedYear} year.`);
  };
  // Creating instances of School
  const school1 = new School('Abc', 1988);
  const school2 = new School('xyz', 1997);
  // Calling the schoolInfo method on the instances
  school1.schoolInfo(); // Output: The school Abc established in 1988 year.
  school2.schoolInfo(); // Output: The school xyz established in 1997 year.

In the above example, we added the schoolInfo method to the School.prototype, and now all instances of School can access and use this method. The method can access the properties of the instance (e.g., this.schoolName and this.establishedYear) because it operates in the context of the instance where it is called.

Using function.prototype.method allows you to avoid duplicating functions for each instance and promotes code reusability, which is one of the key benefits of using constructor functions and prototypes in JavaScript.

Built-in Function Methods:

In JavaScript, functions are first-class citizens, which means they can be assigned to variables, passed as arguments to other functions, and returned as values from functions. This allows us to use function methods, which are built-in functions that can be called on functions themselves or on function objects. Function methods can add additional functionality or provide useful utilities for working with functions. Let’s explore some common function methods with examples:

Function.prototype.call()

The call() method allows you to invoke a function with a specified this value and arguments provided individually.

function displayInfo(title, description) {
  console.log(`Title: ${title}`);
  console.log(`Description: ${description}`);
  console.log(`This object:`, this);
}
const book = {
  title: 'The Great Gatsby',
  author: 'F. Scott Fitzgerald',
};
const movie = {
  title: 'Inception',
  director: 'Christopher Nolan',
};
// Call the 'displayInfo' function with the 'book' object as the context
displayInfo.call(book, 'Book', 'A classic novel about the Jazz Age.');
// Call the 'displayInfo' function with the 'movie' object as the context
displayInfo.call(movie, 'Movie', 'A mind-bending thriller with dream heists.');
/* output:
Title: Book
Description: A classic novel about the Jazz Age.
This object: { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }
Title: Movie
Description: A mind-bending thriller with dream heists.
This object: { title: 'Inception', director: 'Christopher Nolan' }
*/

In this example, we have a displayInfo function that takes two arguments: title and description. When called, it logs the title, description, and the this object (execution context) to the console.

We have two objects: book and movie, representing a book and a movie, respectively. We call the displayInfo function with the book object as the context. The first argument becomes the title and the second argument becomes the description. The this object inside the function refers to the book object. It logs the information about the book. Next we call the displayInfo function with the movie object as the context. The first argument becomes the title, and the second argument becomes the description. The this object inside the function refers to the movie object. It logs the information about the movie.

Using call(), we can dynamically set the context (this) for a function and pass arguments individually when calling the function. This allows us to reuse the same function with different objects and arguments as needed.

Function.prototype.apply()

The apply() method is similar to the call() method, but instead of passing arguments individually, it takes an array-like object as the second argument that contains the arguments to be passed to the function. I’m rewriting the previous example using the apply() method:

function displayInfo(title, description) {
  console.log(`Title: ${title}`);
  console.log(`Description: ${description}`);
  console.log(`This object:`, this);
}
const book = {
  title: 'The Great Gatsby',
  author: 'F. Scott Fitzgerald',
};
const movie = {
  title: 'Inception',
  director: 'Christopher Nolan',
};
// Call the 'displayInfo' function with the 'book' object as the context using 'apply'
displayInfo.apply(book, ['Book', 'A classic novel about the Jazz Age.']);
// Call the 'displayInfo' function with the 'movie' object as the context using 'apply'
displayInfo.apply(movie, ['Movie', 'A mind-bending thriller with dream heists.']);
/* output:
Title: Book
Description: A classic novel about the Jazz Age.
This object: { title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }
Title: Movie
Description: A mind-bending thriller with dream heists.
This object: { title: 'Inception', director: 'Christopher Nolan' }
*/

In this example, we used the apply() method to achieve the same result as with the call() method: We call the displayInfo function with the book object as the context using apply(). The first argument becomes the title, and the second argument becomes the description. The this object inside the function refers to the book object. It logs the information about the book. Next we call the displayInfo function with the movie object as the context using apply(). The first argument becomes the title, and the second argument becomes the description. The this object inside the function refers to the movie object. It logs the information about the movie.

The primary difference between call() and apply() is the way arguments are passed to the function: call() takes them individually, while apply() takes them as an array-like object. Otherwise, their behavior is similar, allowing you to set the context (this) dynamically when calling a function.

Function.prototype.bind()

The bind() method creates a new function with a specified this value and, optionally, initial arguments. The binding is not permanent in the sense that it cannot be changed once the function is bound. Once a function is bound to a specific context using bind(), it will always execute in that context, regardless of how or where it is called. However, it is important to note that you can bind the same function to different objects, resulting in different contexts for each bound function.

// A simple calculator object
const calculator = {
  result: 0,
  add: function (x, y) {
    this.result = x + y;
    console.log(`Result (Calculator): ${this.result}`);
  },
};
// Create a bound version of the 'add' method with a fixed 'this' context and first argument
const addToFive = calculator.add.bind(calculator, 5);
// Create a new object
const anotherCalculator = {
  result: 0,
};
// Bind the function to the new 'anotherCalculator' object
const addToAnotherCalculator = calculator.add.bind(anotherCalculator, 5);
// Use the bound functions
addToFive(3); // Output: Result (Calculator): 8
addToAnotherCalculator(3); // Output: Result (Calculator): 8
// Log the results from the objects
console.log(`Final Result (Calculator): ${calculator.result}`); // Output: Final Result (Calculator): 8
console.log(`Final Result (Another Calculator): ${anotherCalculator.result}`); // Output: Final Result (Another Calculator): 8

In this example, we have a calculator object with an add method that performs an addition operation.

We create a bound version of the add method with a fixed this context (the calculator object) and the first argument set to 5. This results in the addToFive function. Next, we create a new object called anotherCalculator. We bind the original calculator object’s add method to the anotherCalculator object, creating the addToAnotherCalculator function. We call the bound functions, addToFive(3) and addToAnotherCalculator(3). Both functions affect their respective objects’ result property. Since both functions were initially bound to different objects (calculator and anotherCalculator), they maintain separate states.We log the final results from both objects. The calculator object’s result is 8 because it was affected by the addToFive(3) operation. The anotherCalculator object’s result is also 8 because it was affected by the addToAnotherCalculator(3) operation.

This demonstrates how binding the same function to different objects allows us to maintain separate states and results for each object, even though they share the same function.

Function.prototype.toString()

The toString() method returns the source code of the function as a string.

function add(a, b) {
  return a + b;
}
const addString = add.toString();
console.log(addString); // Output: "function add(a, b) { return a + b; }"

Function.prototype.length

The length property returns the number of arguments expected by the function.

function add(a, b) {
  return a + b;
}
console.log(add.length); // Output: 2

Function.prototype.caller

The caller property returns the function that invoked the current function.

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);
}
outer(); // Output: function outer() { inner(); }

In modern JavaScript, accessing inner.caller is considered non-standard and discouraged due to security concerns and the complexity of function call stacks. It is recommended to use alternative approaches, such as passing the context explicitly or using callback functions, for similar functionalities.

Generator Functions:

Generator functions are a special type of function that can be paused and resumed. They use the yield keyword to produce a sequence of values over time. Generator functions are denoted by an asterisk (*) after the function keyword.

function* dogNames() {
  yield "Coco";
  yield "Rusty";
  yield "Smile";
}
var dogName = dogNames();
console.log(dogName.next().value); // Output: Coco
console.log(dogName.next().value); // Output: Rusty
console.log(dogName.next().value); // Output: Smile

Callback Functions:

In JavaScript, callback functions are functions that are passed as arguments to other functions and are executed later, often asynchronously. They are commonly used in event handling, asynchronous operations, and functional programming.

Sure! Here’s an example of a callback function in JavaScript:

// Function that takes a callback as an argument
function doSomethingAsync(callback) {
  console.log("Doing something asynchronously...");
  setTimeout(function() {
    console.log("Async task completed!");
    callback(); // Calling the callback function
  }, 2000); // Simulating an asynchronous task that takes 2 seconds
}
// Callback function to be passed as an argument
function callbackFunction() {
  console.log("Callback function called!");
}
// Using the doSomethingAsync function with the callback
doSomethingAsync(callbackFunction);

In this example, we have a function called doSomethingAsync that takes a callback function as an argument. The doSomethingAsync function simulates an asynchronous task using setTimeout (e.g., making an HTTP request, reading a file, etc.). After the task is completed (after 2 seconds in this case), it calls the callback function provided. We then define a separate function called callbackFunction, which serves as the callback to be passed to doSomethingAsync. Finally, we call doSomethingAsync and pass the callbackFunction as the argument. When the asynchronous task is completed, the callbackFunction is invoked by doSomethingAsync, and it logs “Callback function called!” to the console.

This is a basic example of how callback functions are used in JavaScript to handle asynchronous operations and perform actions after a task is completed. Callbacks are a fundamental concept in JavaScript and are widely used in various scenarios, including event handling, AJAX requests, and more.

For more read How to return response from asynchronous call

Function Composition:

Function composition is the process of combining multiple functions to create a new function. It allows you to break complex tasks into smaller, reusable functions.

function add(a, b) {
  return a + b;
}
function multiply(a, b) {
  return a * b;
}
function square(n) {
  return n * n;
}
var composedFunction = function(a, b) {
  var sum = add(a, b);
  var product = multiply(sum, sum);
  return square(product);
};
console.log(composedFunction(2, 3)); // Output: 25

Named Function Expressions:

A named function expression is a type of function expression in JavaScript where the function being created has a specified name. In contrast to function declarations, function expressions allow functions to be defined as part of an expression, rather than a statement.

The syntax of a named function expression looks like this:

var functionName = function functionExpressionName(parameters) {
  // Function body
  // Code goes here
};

In a named function expression, the functionExpressionName is the name given to the function, and it can be used inside the function to refer to itself. However, the name is not accessible outside the function expression and is usually optional.

Here’s an example of a named function expression:

var addNumbers = function sum(a, b) {
  return a + b;
};
console.log(addNumbers(2, 3)); // Output: 5

In this example, addNumbers is a named function expression. The name sum is optional and can be used to reference the function from inside the function body, allowing for recursion or other self-referential operations.

Named function expressions are useful in situations where you need to define functions as part of an expression, and they provide a way to give a name to an otherwise anonymous function. They are often used for better code organization, debugging, and recursion. However, it’s important to note that the function name is not accessible outside the function expression and does not affect the variable scope.

The arguments Object:

Inside a function, the arguments object is available and contains an array-like list of arguments passed to the function. It allows you to access arguments dynamically, regardless of the number of declared parameters.

function sum() {
  console.log(arguments);
  var total = 0;
  for (var i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}
console.log(sum(1, 2, 3)); // Output: 6
console.log(sum(4, 5, 6, 7)); // Output: 22
/*
[Arguments] { '0': 1, '1': 2, '2': 3 }
6
[Arguments] { '0': 4, '1': 5, '2': 6, '3': 7 }
22
*/

Function Generators:

Function generators are a powerful feature introduced in ES6. They allow you to define functions that can pause and resume their execution. Generator functions are defined using an asterisk (*) after the function keyword and use the yield keyword to control the flow.

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}
var generator = generateSequence();
console.log(generator.next().value); // Output: 1
console.log(generator.next().value); // Output: 2
console.log(generator.next().value); // Output: 3

Named Function Parameters:

In JavaScript, you can use named function parameters when calling a function. Named parameters allow you to pass arguments to a function in a more explicit and readable way.

function greet({ name, age }) {
  console.log("Hello, " + name + "! You are " + age + " years old.");
}
greet({ name: "Kat", age: 25 }); // Output: Hello, Kat! You are 25 years old.

Method Shorthand in Objects:

When defining methods in an object, you can use the shorthand syntax in ES6. Instead of explicitly defining a function using the function keyword, you can omit it if the method name is followed by parentheses.

var person = {
  name: "Kat",
  greet() {
    console.log("Hello, " + this.name + "!");
  }
};
person.greet(); // Output: Hello, Kat!

Rest Properties:

ES2018 introduced the concept of rest properties, which allows you to gather the remaining properties of an object into a new object. It is denoted by three dots (...) followed by an object name.

var { name, age, ...rest } = { name: "Kat", age: 30, city: "New York", country: "USA" };
console.log(name); // Output: Kat
console.log(age); // Output: 30
console.log(rest); // Output: { city: "New York", country: "USA" }

You can read more about rest parameter and spread operator here

Immediately Resolved Promises:

Promises are a way to handle asynchronous operations in JavaScript. You can create an immediately resolved promise using the Promise.resolve() method. It returns a promise that is already resolved with the specified value.

Promise.resolve("Hello, World!")
  .then(result => console.log(result)); // Output: Hello, World!

Async/Await:

Async/await in JavaScript is used to simplify asynchronous code handling, particularly when dealing with Promises. It provides a more synchronous-looking syntax for working with asynchronous operations, making the code easier to read and understand.

The async keyword is used to define an asynchronous function. When a function is declared as async, it automatically returns a Promise. Inside an async function, you can use the await keyword. The await keyword is used inside an async function to wait for the completion of a Promise. It can only be used inside an async function.

Using await, you can pause the execution of the async function until the Promise is resolved. When the Promise is resolved, the value it resolves with will be returned, or if the Promise is rejected, an error will be thrown.

An asynchronous operation refers to a task or process that does not block the execution of other tasks in a program. In other words, when an operation is asynchronous, it doesn’t wait for the task to complete before moving on to the next line of code. Instead, it allows the program to continue executing other tasks or operations while waiting for the asynchronous task to finish.

In many programming scenarios, certain operations can take a significant amount of time to complete, such as reading data from a file, making an API call, or fetching data from a remote server. If these operations were performed synchronously (i.e., blocking), the entire program’s execution would halt until the operation completes. During this waiting time, the program wouldn’t be able to perform any other tasks or respond to user input, resulting in a poor user experience and inefficient use of resources.

Asynchronous operations are particularly crucial in web development, where actions like handling user interactions, fetching data from servers, or performing time-consuming computations can cause delays. By making such operations asynchronous, the program can continue executing other tasks or respond to user input while waiting for the time-consuming operation to finish. This allows for a more responsive and efficient user experience.

In JavaScript, asynchronous operations are often handled using Promises, callbacks, or modern features like async/await, allowing us to write non-blocking code and manage the flow of asynchronous tasks effectively. These mechanisms ensure that the program remains responsive, even when dealing with time-consuming operations, making it well-suited for web applications and other scenarios with potentially long-running tasks.

Here’s an example of using async/await with a Promise-based function:

function fetchData() {
  return new Promise((resolve, reject) => {
    // Simulate an asynchronous operation (e.g., API call, file read)
    setTimeout(() => {
      const data = { name: 'Kat', age: 30 };
      resolve(data);
    }, 2000);
  });
}
async function fetchAndProcessData() {
  try {
    // Wait for the Promise to resolve and store the result in 'result'
    const result = await fetchData();
    // Do something with the result
    console.log(`Name: ${result.name}, Age: ${result.age}`);
  } catch (error) {
    // Handle errors if the Promise is rejected
    console.error('Error fetching data:', error);
  }
}
// Call the async function
fetchAndProcessData();

In this code example, fetchData() returns a Promise that resolves after a simulated 2-second delay. Inside the fetchAndProcessData() function, we use await to pause execution until the Promise is resolved or rejected. This allows us to work with the result of the Promise in a more straightforward and synchronous-like manner.

In the above code example I provided, there are three keywords related to Promises i.e resolve, reject and promise.

Promise is a built-in JavaScript constructor function that represents a future value or result of an asynchronous operation. It provides a way to interact with asynchronous operations more effectively by using its methods, like then() and catch(). In the code example, the fetchData function returns a new Promise instance, which can be used to handle the result of the asynchronous operation using the then() method when it resolves successfully, or the catch() method when it is rejected with an error.

resolve is a function that is part of the Promise’s internal mechanism. It is used to fulfill a Promise and pass the result of the asynchronous operation when it succeeds. When you call resolve(value) inside a Promise, the Promise transitions from the pending state to the fulfilled state with the provided value. In the code example, resolve(data) is called when the asynchronous operation (simulated by setTimeout) successfully completes with the data object.

reject is another function that is part of the Promise’s internal mechanism. It is used to reject a Promise and indicate that an error occurred during the asynchronous operation. When you call reject(error) inside a Promise, the Promise transitions from the pending state to the rejected state with the provided error. In the code example, if something goes wrong in the simulated asynchronous operation, the reject function would be called with the error object.

resolve and reject are functions provided by the Promise mechanism to handle the success or failure of asynchronous operations, and Promise is a built-in JavaScript constructor for creating and interacting with Promises.

Async/await is especially useful when you have to deal with multiple asynchronous operations in sequence or need to handle errors more cleanly. It simplifies the code compared to using traditional Promise chaining or nested callbacks.

You can read more about asynchronous call in JavaScript here.

Tail-Call Optimization:

It allows recursive functions to be optimized to prevent stack overflow errors by reusing the existing stack frame. However, not all engines support TCO. Here’s an example of a tail-recursive function:

function factorial(n, acc = 1) {
  if (n === 0) {
    return acc;
  }
  return factorial(n - 1, acc * n);
}
console.log(factorial(5)); // Output: 120

Generally , each function call creates a new stack however when calling tail call optimized function, the same stack is reused. It replaces the current arguments with new arguments in the same stack. You can read more about tail call optimization here .

I hope this blog have helped you understand different functions in JavaScript.

Also Read Variadic Functions in JavaScript

Read debounce function and setTimeout function in Javascript

Don't Miss Out! Subscribe to Read Our Latest Blogs.

If you found this blog helpful, share it on social media.

Subscription form (#5)

Pin It on Pinterest

Scroll to Top