Skip to main content

JavaScript Decorators: Enhancing Functions and Classes | Ultimate Guide

ยท 7 min read

"JavaScript Decorators: Enhancing Functions and Classes | Ultimate Guide"

Introductionโ€‹

JavaScript decorators are a powerful feature that enable you to modify, extend, or enhance functions and classes in a clean and reusable manner. Borrowed from the world of Python, decorators provide a flexible way to apply behaviors such as logging, authentication, or validation to your code.

In this article, we'll explore the concept of decorators, their benefits, and practical examples to demonstrate how they can elevate your JavaScript codebase.

Suggested Tutorials ๐Ÿ“‘:โ€‹

What are Decorators?โ€‹

Decorators are a special kind of function that can be used to modify, enhance, or extend the behavior of other functions or classes. They are a form of metaprogramming, which is a technique that allows you to modify the behavior of a program at runtime.

Benefits of Decoratorsโ€‹

Modularity: Decorators encapsulate behaviors, making it easy to apply them selectively to different functions or classes.

Reusability: Decorators can be reused across different parts of your codebase, promoting a consistent approach.

Readability: Decorators enhance code readability by separating core logic from additional concerns.

Extensibility: Decorators can be used to extend the functionality of existing functions or classes without modifying them directly.

1. Function Decoratorsโ€‹

Function decorators are used to modify the behavior of a function. They are declared using the @ symbol followed by the name of the decorator function. The decorator function is then applied to the target function, which is passed as an argument to the decorator function.

Let's look at a simple example of a function decorator that logs the name of the function and its arguments to the console.


function log(target, name, descriptor) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function (...args) {
console.log(`Arguments for ${name}: ${args}`);
try {
const result = original.apply(this, args);
console.log(`Result from ${name}: ${result}`);
return result;
} catch (e) {
console.log(`Error from ${name}: ${e}`);
throw e;
}
}
}
return descriptor;
}

class Example {
@log
sum(a, b) {
return a + b;
}
}

const e = new Example();
e.sum(1, 2);

// Arguments for sum: 1,2
// Result from sum: 3

In the above example, we define a decorator function called log that takes three arguments: target, name, and descriptor. The target argument refers to the class that contains the method being decorated. The name argument refers to the name of the method being decorated. The descriptor argument is an object that contains the method's properties.

The log decorator function then checks if the descriptor value is a function. If it is, the decorator function replaces the original function with a new function that logs the name of the function and its arguments to the console. The decorator function then calls the original function and logs the result to the console.

Finally, we apply the log decorator to the sum method of the Example class. When we call the sum method, the decorator function is invoked and logs the name of the method and its arguments to the console. The decorator function then calls the original sum method and logs the result to the console.

Suggested Tutorials ๐Ÿ“‘:โ€‹

2. Class Decoratorsโ€‹

Class decorators are used to modify the behavior of a class. They are declared using the @ symbol followed by the name of the decorator function. The decorator function is then applied to the target class, which is passed as an argument to the decorator function.

Let's look at a simple example of a class decorator that logs the name of the class and its constructor arguments to the console.


function log(target) {
const original = target;

function construct(constructor, args) {
const c: any = function () {
return constructor.apply(this, args);
}
c.prototype = constructor.prototype;
return new c();
}

const f: any = function (...args) {
console.log(`Arguments for ${original.name}: ${args}`);
return construct(original, args);
}

f.prototype = original.prototype;
return f;
}

@log
class Example {
constructor(a, b) {
console.log('constructor');
}
}

const e = new Example(1, 2);

// Arguments for Example: 1,2
// constructor

In the above example, we define a decorator function called log that takes one argument: target. The target argument refers to the class that is being decorated. The log decorator function then replaces the original class with a new class that logs the name of the class and its constructor arguments to the console. The decorator function then calls the original class and logs the result to the console.

Finally, we apply the log decorator to the Example class. When we instantiate the Example class, the decorator function is invoked and logs the name of the class and its constructor arguments to the console. The decorator function then calls the original Example class and logs the result to the console.

Suggested Tutorials ๐Ÿ“‘:โ€‹

3. Decorator Factoriesโ€‹

Decorator factories are used to create decorators that accept arguments. They are declared using the @ symbol followed by the name of the decorator function. The decorator function is then applied to the target function or class, which is passed as an argument to the decorator function.


function log(message) {
return function (target, name, descriptor) {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function (...args) {
console.log(message);
try {
const result = original.apply(this, args);
console.log(`Result from ${name}: ${result}`);
return result;
} catch (e) {
console.log(`Error from ${name}: ${e}`);
throw e;
}
}
}
return descriptor;
}
}


class Example {
@log('Hello from Example')
sum(a, b) {
return a + b;
}
}

const e = new Example();
e.sum(1, 2);

// Hello from Example
// Result from sum: 3

In the above example, we define a decorator factory called log that takes one argument: message. The log decorator factory then returns a decorator function that takes three arguments: target, name, and descriptor. The target argument refers to the class that contains the method being decorated. The name argument refers to the name of the method being decorated. The descriptor argument is an object that contains the method's properties.

The log decorator function then checks if the descriptor value is a function. If it is, the decorator function replaces the original function with a new function that logs the message to the console. The decorator function then calls the original function and logs the result to the console.

Finally, we apply the log decorator to the sum method of the Example class. When we call the sum method, the decorator function is invoked and logs the message to the console. The decorator function then calls the original sum method and logs the result to the console.

Conclusionโ€‹

JavaScript decorators provide an elegant way to enhance functions and classes without cluttering your core logic. Whether you're adding logging, validation, or other cross-cutting concerns, decorators promote modularity and reusability. By integrating decorators into your coding practices, you can craft more maintainable and extensible codebases, enriching the functionality of your JavaScript applications while maintaining a clear separation of concerns.

We hope you enjoyed this article on JavaScript decorators.

Happy coding! ๐Ÿ™Œ

Suggested Tutorials ๐Ÿ“‘:โ€‹