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! ๐