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 Decorator Examples
- Class Decorators in JavaScript
- Use case of Decorator Functions in JavaScript
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:
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:
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.
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:
- Class Member Decorator
- 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.
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);
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 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.