Introduction to Decorator Function In JavaScript

JavaScript has been the silent revolution in the world of web development. It has made creating new websites much faster and easier for developers.

Features such as decoration function in javascript have blessed developers with scalability and adaptability to new user demands effectively.

Just imagine the joy of customizing your T-shirt logo however you please whenever you have a new idea, that’s how easy scaling the website functionalities with the decorator function is!

With this blog, we will take a look at ways you can use these decorator functions to create effective functions for your web development project.

Table of Contents

JavaScript Decorator Function: Brushing up the Basics

JavaScript decorators allow you to effectively add brand-new behaviors and features to pre-existing functions or classes. This makes scaling the existing features easier.

For understanding in simpler tech terms, you can think of decorators as a wrapper around the base code which adds up to the original functionality of it.

Note: "The implementation of function decorators is possible in JS as it supports higher-order functions. However, for class decorators adapting the same would add up to extra complications. Hence we use smart alternative tools such as Babel to implement decorators within the classes".

At this point, you might wonder why we need a decorator if we can just alter or create brand-new functions. Decorators offer multiple long terms benefits to developers such as:

  • A cleaner approach to wrapping and upgrading functions.
  • Increases code reusability by separating decorator from main code which allows them to be used in multiple instances.
  • Helps in modifying classes when needed without any complexity.

JavaScript Decorator Examples

Now that you know all about the perks of using JavaScript decorators let's now take a further in-depth look into each of the areas where you can implement decorators with relevant working examples.

You can consider function decorators as additional functions which can easily take the core function in your module as an argument and enhance its core functionality.

1. Functions assigned to a Variable

Here's an example of a decorator function in JavaScript that takes a function assigned to a variable and wraps it with additional functionality:

function logDecorator(originalFunc) {
  return function(...args) {
    console.log(`Calling ${originalFunc.name} with arguments:`, args);
    const result = originalFunc.apply(this, args);
    console.log(`Returned value from ${originalFunc.name}:`, result);
    return result;
  };
}

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

add = logDecorator(add);

console.log(add(1, 2));

In the above example, the logDecorator function is a decorator that takes a function assigned to a variable as its parameter and returns a new function that wraps the original function with additional logging functionality.

The returned function logs information about the original function call and its return value, and then calls the original function using apply to ensure that this is bound correctly.

The add function is assigned to a variable using the let keyword. The add function is then decorated by calling logDecorator and passing in add as the parameter. The resulting decorated function is then assigned back to the add variable.

When add is called with arguments, it will log information about the original function call and its return value, as well as return the result of the original function.

Output:

JavaScript Decorator Function
JavaScript Decorator Function

As you can see, the decorator function is able to add behavior to the traditional function without modifying its source code directly.

This can be useful for adding cross-cutting concerns to functions, such as logging or performance measurement, without cluttering the original code with that logic.

2. Functions passed as a Parameter to another Function

A common pattern in decorator functions is taking a function as a parameter and wrapping it with additional functionality. Here's an example in JavaScript.

function withRetry(originalFunc, maxAttempts) {
  return async function(...args) {
    let attempt = 1;

    while (attempt <= maxAttempts) {
      try {
        console.log(`Attempt ${attempt}: Calling ${originalFunc.name} with arguments:`, args);
        const result = await originalFunc.apply(this, args);
        console.log(`Attempt ${attempt}: Returned value from ${originalFunc.name}:`, result);
        return result;
      } catch (error) {
        console.log(`Attempt ${attempt}: Error occurred during ${originalFunc.name}:`, error.message);
        attempt++;
      }
    }

    throw new Error(`Exceeded maximum attempts (${maxAttempts}) for ${originalFunc.name}`);
  };
}

async function fetchWithRetry(url, maxAttempts) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`Fetch error: ${response.status} ${response.statusText}`);
  }

  return response.json();
}

const fetchWithRetry2 = withRetry(fetchWithRetry, 3);

(async () => {
  try {
    const result = await fetchWithRetry2('https://jsonplaceholder.typicode.com/todos/1', 3);
    console.log(result);
  } catch (error) {
    console.error(error.message);
  }
})();

In the following instance, the withRetry function is a decorator that takes a function as its parameter and a maxAttempts value, and returns a new function that wraps the original function with retry functionality.

The returned function uses a while loop to attempt to call the original function and handle any errors that occur. If the original function succeeds, the function returns the result. If an error occurs, the function logs the error message and retries the original function up to the maximum number of attempts specified. If the maximum number of attempts is reached and the function still fails, an error is thrown.

The fetchWithRetry function is an asynchronous function that fetches data from a URL and returns the parsed JSON response. It's used to demonstrate how the withRetry decorator can be applied to an existing function.

The decorator is applied to fetchWithRetry by calling withRetry and passing in fetchWithRetry as the first parameter and a maximum of 3 attempts as the second parameter. The resulting decorated function is assigned to a new variable, fetchWithRetry2.

The fetchWithRetry2 function is called with a URL and a maximum of 3 attempts. If the first attempt fails, the function will retry up to 3 times before throwing an error.

Output:

Decorator Function
Decorator Function

3. Function returned by another Function

An example of a decorator function in JavaScript is one that adds extra functionality to an existing function by returning a new function that wraps the original function.

function debounceDecorator(func, delay) {
  let timeoutId;

  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

function expensiveFunction() {
  console.log('Expensive function called!');
}

const debouncedFunction = debounceDecorator(expensiveFunction, 1000);

// This will call the expensive function immediately:
expensiveFunction();

// This will call the expensive function after a delay of 1000 milliseconds:
debouncedFunction();

In this example, the debounceDecorator function is a decorator that takes a function as its parameter and a delay value in milliseconds, and returns a new function that wraps the original function with debouncing functionality.

The returned function sets a timeout using setTimeout to delay the execution of the original function by the specified delay time. If the returned function is called again before the timeout expires, the previous timeout is cleared using clearTimeout and a new timeout is set.

The expensiveFunction is a function that simulates an expensive operation, and it will be called by the debounced function.

Decorator Function in JavaScript
Decorator Function in JavaScript

The decorator is applied to the expensiveFunction by calling debounceDecorator and passing in expensiveFunction as the first parameter and a delay value of 1000 as the second parameter. The resulting debounced function is assigned to a new variable, debouncedFunction.

When expensiveFunction is called directly, it will execute immediately. However, when debouncedFunction is called, it will delay the execution of expensiveFunction by 1000 milliseconds.

If debouncedFunction is called again before the delay expires, the previous execution of expensiveFunction will be cancelled and a new delay will be set. This can be useful in scenarios where a function is called frequently but expensive to execute, and it's important to avoid overwhelming the system with too many calls at once.

Class Decorators in JavaScript

As explained above decorators can also be easily used to decorate class values as well however the method would be quite different than that used in functions. You can use two major types of decorators that you can use in your JS class:

  1. Class Member Decorator
  2. Member of Class Function

We will take an in-depth look into each of these using reliable examples to gain further understanding.

Note: However please note before trying the example below using the Bable+JSX+JS library and install the @babel/plugin-proposal-decorators plugin to use decorators effectively or you can directly use Typescript as well.

Also, you can further use the Javascript Proposal Decorator playground to experiment with decorators without any complications.

For the examples below, we will be using a mix of JS fiddle editor and Javascript proposal decorator. However, you can use any editor of your choice.

1. Class Member Decorator

Class member decorators are usually applied to only singular class members. Your class member decorator contains properties, methods, getters, and setters to enable easy manipulation of class elements.

To be able to use the class member decorator you will need to use three parameters named target, class, and descriptor.

  • The target parameter helps you to access the class whose member you will alter or perform operations on.
  • The name parameter helps you access the name of the class member that will be affected.
  • The descriptor helps you pass the object into the element for final execution.

Here's an example of a class decorator in JavaScript that adds a new method to a class and logs information about the class and method:

function logMethod(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`Calling ${name} with arguments:`, args);
    return originalMethod.apply(this, args);
  };
  return descriptor;
}

class MyClass {
  existingMethod(a, b) {
    return a + b;
  }
}

logMethod(MyClass.prototype, "existingMethod", Object.getOwnPropertyDescriptor(MyClass.prototype, "existingMethod"));

const myInstance = new MyClass();
console.log(myInstance.existingMethod(1, 2));

In this example, the logMethod function is a class decorator that takes three arguments: target, name, and descriptor. The target argument is the class prototype, name is the name of the method being decorated, and descriptor is the property descriptor of the method.

The decorator modifies the value property of the method descriptor to log information about the method when it is called. The original method is called using apply to ensure that this is bound correctly.

The decorator is then applied to the existingMethod method of the MyClass class by calling logMethod and passing in the class prototype, method name, and property descriptor.

Class Member Decorator
Class Member Decorator

When an instance of the MyClass class is created and the existingMethod method is called, information about the method call will be logged to the console because of the decorator.

2. Member of Class Function

Member of class decorator applies changes and new functionalities to the whole class, these can be called using a single parameter to the fields that you wish to alter.

However, these decorators can only be applied to constructor functions which limits its scope of functionalities hence making the above-mentioned class member function a much more versatile option to adapt for altering classes in JavaScript effectively.

In this example, we will create a class that shows the name of an employee passed through argument along with their employment verification passed by the decorator to modify the existing class.

Note: To work with this use JS-Proposal-Decorator Playground.

@isEmployee
class Greets {
  constructor(name) {
    this.name = name;
  }
  hello() {
    console.log (`hey ${this.name}`);
  }
}

function isEmployee(target) {
  return class extends target {
    constructor(...args) {
      super(...args);
      this.isEmployee = true;
    }
  };
}

const greeting = new Greets ('Jenna');
console.log (greeting);
Member of a Class function
Member of a Class function

You can check the output result here.

Usecase of Decorator function in JavaScript

Decorator functions are a popular programming pattern in JavaScript, and they are often used in the following use cases:

1. Logging

They can be used to log function calls, inputs, and outputs. For example, you can define a logging decorator that takes a function as input and logs its inputs and outputs to the console.

function logDecorator(func) {
  return function() {
    console.log("Function called with arguments: ", arguments);
    const result = func.apply(this, arguments);
    console.log("Function returned: ", result);
    return result;
  }
}

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

const loggedAdd = logDecorator(add);

loggedAdd(2, 3);
// Output:
// Function called with arguments:  { '0': 2, '1': 3 }
// Function returned:  5

2. Caching

Decorator functions can be used to cache function results for better performance. For example, you can define a caching decorator that takes a function as input and caches its results based on its input arguments.

function cacheDecorator(func) {
  const cache = new Map();
  return function() {
    const key = JSON.stringify(arguments);
    if (cache.has(key)) {
      console.log("Result returned from cache.");
      return cache.get(key);
    }
    const result = func.apply(this, arguments);
    cache.set(key, result);
    console.log("Result cached.");
    return result;
  }
}

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const cachedFibonacci = cacheDecorator(fibonacci);

cachedFibonacci(10);
// Output:
// Result cached.
// 55

cachedFibonacci(10);
// Output:
// Result returned from cache.
// 55

Authorization

Decorator functions can be used to add authorization checks to functions. For example, you can define an authorization decorator that takes a function as input and checks if the user is authorized to call it.

function authDecorator(func) {
  return function() {
    const user = getUser(); // assume this function returns the current user
    if (!user || !user.isAdmin) {
      throw new Error("Unauthorized access.");
    }
    return func.apply(this, arguments);
  }
}

function deletePost(postId) {
  // delete the post with the given ID
}

const authorizedDeletePost = authDecorator(deletePost);

authorizedDeletePost(123);
// Throws "Unauthorized access." if the current user is not an admin.

These are just a few examples of how decorator functions can be used in JavaScript. Decorator functions are a powerful tool for extending and modifying the behavior of functions in a reusable and composable way.

Bonus Info:

The class decorators are not a part of mainstream javascript yet. The decorators are still at their proposal stage and are currently passing through stage 3 of the proposal (i.e. we have a basic draft of all the decorators which is yet to be announced officially).

You can read all about its features here. To be future-ready, it's highly recommended developers understand the functionalities of decorators so that they can use them to speed up their development process.

Wrapping Up On Decorators

JavaScript decorators can help you in adding new features to both functions and classes. This, in the modern development process where multiple iterations are needed, ensures easier scalability for web developers even if they use basic JS to create their web apps. Hence making them an essential element.

Although JS decorator functions are not officially declared yet and are in the proposal stage, you can still work with them.

Further, the same concept of decorator can also be implemented on multiple JS derivatives as well such as Typescript, React, and Angular as well. Hence, understanding Decorators essential can help you not only in JS but also while working with any JS-Based frontend technology.

So try out the decorator function mentioned above to expand your overall understanding of decorators today for faster and more efficient development.


Node.js Performance Monitoring with Atatus

Atatus keeps track of your Node.js application to give you a complete picture of your clients' end-user experience. You can determine the source of delayed response times, database queries, and other issues by identifying backend performance bottlenecks for each API request.

Node.js application monitoring
Node.js application monitoring

Node.js performance monitoring made bug fixing easier, every Node.js error is captured with a full stack trace and the specific line of source code marked. To assist you in resolving the Node.js error, look at the user activities, console logs, and all Node.js requests that occurred at the moment. Error and exception alerts can be sent by email, Slack, PagerDuty, or webhooks.

Try Atatus’s entire features free for 14 days.

Vaishnavi

Vaishnavi

CMO at Atatus.
Chennai

Monitor your entire software stack

Gain end-to-end visibility of every business transaction and see how each layer of your software stack affects your customer experience.