decorator is a design pattern that allows you to modify the behavior of an object or function without directly modifying its source code. It involves wrapping a function or object with another function that can modify its behavior or add new functionality.
There are different ways to create a decorator in JavaScript, but one of the most common ways is to use higher-order functions, which are functions that take one or more functions as arguments and/or return a function.
Let’s look at an example of a simple decorator that adds logging functionality to a function:
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with arguments ${args}`);
const result = fn.apply(this, args);
console.log(`Result of ${fn.name}: ${result}`);
return result;
}
}
function add(a, b) {
return a + b;
}
const addWithLogging = withLogging(add);
console.log(addWithLogging(2, 3)); // logs: Calling add with arguments 2,3, Result of add: 5, 5
In this example, withLogging
is a decorator function that takes a function fn
as an argument and returns a new function that wraps fn
with logging functionality. The returned function takes any number of arguments using the rest parameter syntax ...args
, logs the function name and arguments, calls the original function with the apply
method, logs the result, and finally returns the result.
We can use the decorator by calling withLogging
with the original function add
, which returns a new function with logging behavior. We can then call the new function as usual with any arguments, and the logging will be automatically added.
Another example of a decorator is one that adds memoization to a function, which is a technique that caches the results of a function for a given set of arguments so that the function doesn’t have to be re-executed every time it’s called with the same arguments:
function withMemoization(fn) {
const cache = new Map();
return function(...args) {
const cacheKey = JSON.stringify(args);
if (cache.has(cacheKey)) {
console.log(`Returning cached result for ${fn.name} with arguments ${args}`);
return cache.get(cacheKey);
} else {
console.log(`Calling ${fn.name} with arguments ${args}`);
const result = fn.apply(this, args);
console.log(`Result of ${fn.name}: ${result}`);
cache.set(cacheKey, result);
return result;
}
}
}
function fibonacci(n) {
if (n < 2) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
const fibonacciWithMemoization = withMemoization(fibonacci);
console.log(fibonacciWithMemoization(10)); // logs: Calling fibonacci with arguments 10, Result of fibonacci: 55, 55
console.log(fibonacciWithMemoization(10)); // logs: Returning cached result for fibonacci with arguments 10, 55
In this example, withMemoization
is a decorator function that takes a function fn
as an argument and returns a new function that adds memoization functionality to fn
. The returned function uses a Map
to cache the results of the function for a given set of arguments. If the cache contains the result for the given arguments, the cached value is returned, otherwise the original function is called with the apply
method, the result is cached, and the result is returned.
We can use the decorator by calling withMemoization
with the original function fibonacci
, which returns a new function
More examples of decorator functions
Timing Decorator: This decorator can be used to measure the time taken by a function to execute. It can be useful for optimizing the performance of your code.
function timingDecorator(func) {
return function(...args) {
console.time(func.name);
const result = func(...args);
console.timeEnd(func.name);
return result;
}
}
function myFunction() {
// some code here
}
const timedFunction = timingDecorator(myFunction);
timedFunction(); // logs the time taken by myFunction to execute
Validation Decorator:
This decorator can be used to validate the input of a function before executing it. It can help prevent errors and ensure that the function receives valid input.
function validationDecorator(func) {
return function(...args) {
if (args.some(arg => typeof arg !== 'number')) {
throw new Error('Invalid input: all arguments must be numbers');
}
return func(...args);
}
}
function multiply(x, y) {
return x * y;
}
const validatedMultiply = validationDecorator(multiply);
validatedMultiply(2, 3); // returns 6
validatedMultiply(2, '3'); // throws an error
Authorization Decorator: This decorator can be used to check if a user is authorized to execute a function. It can help ensure that only authorized users have access to sensitive functionality.
function authorizationDecorator(func) {
return function(user, ...args) {
if (!user.isAdmin) {
throw new Error('Unauthorized: only admins can execute this function');
}
return func(...args);
}
}
function deleteRecord(id) {
// some code here
}
const user = { name: 'John', isAdmin: false };
const admin = { name: 'Jane', isAdmin: true };
const authorizedDelete = authorizationDecorator(deleteRecord);
authorizedDelete(user, 123); // throws an error
authorizedDelete(admin, 123); // executes the function
another example of authorization decorator
It can be useful for implementing role-based access control (RBAC) in your application.
function authorizationDecorator(role) {
return function(func) {
return function(...args) {
if (user.role === role) {
return func(...args);
} else {
throw new Error('Unauthorized access');
}
}
}
}
const user = {
role: 'admin'
};
@authorizationDecorator('admin')
function myFunction() {
// some code here
}
myFunction(); // executes only if user.role === 'admin'
Delay Decorator
Here’s an example of a decorator function that can delay the execution of another function by a specified amount of time:
function delay(fn, delayTime) {
return function(...args) {
setTimeout(() => {
fn.apply(this, args);
}, delayTime);
};
}
This delay
function takes in two arguments – the function to be delayed (fn
) and the amount of time to delay the function by (delayTime
). It then returns a new function that will wrap around the original function.
When the new function is called, it uses the setTimeout
method to delay the execution of the original function by the specified amount of time. It then uses the apply
method to call the original function with the correct arguments and this
context.
Here’s an example of how you can use this delay
decorator function:
function greet(name) {
console.log(`Hello, ${name}!`);
}
const delayedGreet = delay(greet, 2000); // delay for 2 seconds
delayedGreet('John'); // output: "Hello, John!" (after 2 seconds)
In this example, we create a new function delayedGreet
by passing the original greet
function and a delay time of 2000
milliseconds to the delay
decorator function. When we call delayedGreet('John')
, it will execute the greet
function after a delay of 2 seconds and output “Hello, John!” to the console.
One thought on “Understanding Decorators in JavaScript with example code”