Attaching new behaviors through decorators in JavaScript

Post Editor

In this article, we will learn about decorators and two different techniques for their implementation - functioning and class-based.

7 min read
1 comment
post

Attaching new behaviors through decorators in JavaScript

In this article, we will learn about decorators and two different techniques for their implementation - functioning and class-based.

post
post
7 min read
1 comment
1 comment

Lots of popular languages such as Java and Python have decorators. It's a common pattern that serves two major purposes:

  • changing the functionality of an object at run-time without impacting the existing functionality of the objects
  • wrapping a behavior into simple, reusable chunks while cutting down the amount of boilerplate code

It's possible to code without decorators as they are mostly syntactic sugar that allow us to wrap and annotate classes and functions, but they greatly improve code readability. One of the common use case for using decorators is adding caching logic to API calls. Check out article for more details.

Decorators are coming to JavaScript as well. It's a new feature that is currently on Stage 2. That's how the proposal define decorators:

Decorators are functions called on classes, class elements, or other JavaScript syntax forms during definition. Decorators have three primary capabilities: They can replace the value that is being decorated with a matching value that has the same semantics. They can associate metadata with the value that is being decorated. This metadata can then be read externally and used for metaprogramming and introspection. They can provide access to the value that is being decorated, via metadata.

Technically, you can think of a decorator in JavaScript as a higher-order function that takes one function as an argument and returns another function as a result. In most implementations the returned function reuses the original function that it takes as a parameter, but that's not necessary.

The standardization process has been moving slowly, but there's progress so it's quite likely we'll see decorators as part of the language in the near future. Meanwhile, TypeScript and Babel can already transpile and polyfill decorators so we can use them in our everyday development. In fact, decorators are becoming popular in a variety of frontend JavaScript frameworks: Angular, Mobx and Vue.js.

The JavaScript proposal defines four types of decorators:

  • class decorator;
  • property decorators;
  • method decorators;
  • accessor decorators.

In this article, we are going to review decorators that are applied to methods in class based implementation and to functions if the functional approach is used. We'll take a look at how it can be done with both approaches using the business requirement to delay a notification when certain conditions hold true.

Implementing decorators
Link to this section

Let's create a class Notification that consists of property type and one simple method notifyUser which displays a type of notification in console  Success notification.

After that, we will create an instance of the class Notification and call notifyUser method:

<>Copy
class Notification { type: string; constructor() { this.type = 'Success'; } notifyUser = function() { console.log(`${this.type} notification`); } } const notification = new Notification ('Success') notification.notifyUser(); // You will see in console - 'Success notification'

Now we need to display this notification message in the console after some delay, for example, 3 seconds.

To implement this new requirement we could introduce a new class DelayedNotification, but it would be almost exactly the same as Notification. It feels like we just need to modify the behavior of the existing Notification class a bit. This is a case when the implementation of the decorator fits perfectly.

Decorator pattern as a higher-order function
Link to this section

First of all, we need to decorate the existing method notifyUser with the setTimeout API. As you remember that setTimeout function takes a number of milliseconds for waiting before executing the code. For that, we need to pass somehow a number of milliseconds to our decorator.

Let's implement a generic higher-order function delayMiliseconds that would take any other function and decorate it by adding a delay in calling it.

<>Copy
const delayMiliseconds = (fn: Function, delay:number = 0) => () => { setTimeout(() => fn(), delay); return 'notifyUser is called'; };

Let's see how we can invoke a decorator:

<>Copy
delayMiliseconds(notification.notifyUser, 3000);

And now to apply this generic function to a class we can do the following changes in our method notifyUser:

<>Copy
class DelayedNotification { type: string; constructor(type) { this.type = type; } public notifyUser = delayMiliseconds(() => { console.log(`${this.type} notification` 'checkTime:' new Date().getSeconds()); }, 3000); } const notification = new DelayedNotification ('Success') console.log(notification.notifyUser() 'checkTime:' new Date().getSeconds())

As a result, we will see the 'Success notification' message in the console after 3 seconds:

Content imageContent image

From this example, we can see that delayMiliseconds is a high-order function -  a function that takes a function as an argument and return another function.

If you look closer at the JavaScript function on arrays, strings, DOM methods, promise method — you could see that many of them are higher-order functions as soon as they accept a function as an argument.

One interesting trait of the decorator pattern applied to classes using higher-order functions is that the behavior is no longer part of the object's prototype. Since we use property definition syntax when declaring a class, class A { prop = value}, instead of method definition syntax, class A { prop() {}, Javascript doesn't add the functionality to the prototype shared by class instances.

For deep investigation let's display an instance of DelayedNotification in the console:

console.log(notification)

Content imageContent image

In order to get a better understanding, I suggest you look in the console - check the method existence of the method notifyUser in notification.

According to documentation, JavaScript first looks for the property at the object itself. If it isn't there, it keeps walking up the prototype chain. hasOwnProperty exists to check the property only at object itself, and not use the prototype chain. If you want to check if a property exists at all, iterating over the prototype chain, use the in operator. Here's what we'll see:

console.log('notifyUser' in notification);  // true

console.log(Object.getPrototypeOf(notification).hasOwnProperty('notifyUser'));  // false

As a result,  at first case ,you see true in the console because  DelayedNotification instance has the method notifyUser.

And in the second case, you see false because the prototype of the notification does not have the method notifyUser.

It is customary with functional approach to not use classes and avoid this , so a factory pattern is a common recipe for creating objects in JS. The higher order function we created above can be aptly used here as well like this:

<>Copy
function functionBasedNotificationFactory() { const type = 'Success'; notifyUser() { console.log(`${type} notification`); } return { name: 'Success', notifyUser: delayMiliseconds(notifyUser, 300); } }
<>Copy
const notification = functionBasedNotificationFactory(); notification.notifyUser(); // 'Success notification' in console after 3 seconds

As you can see from the screenshot above, the factory function functionBasedNotificationFactory return object with decorated method notifyUser.

Class-based approach
Link to this section

Let's take a look at the implementation of the decorator pattern as a language feature.  Class based implementation looks like this:

<>Copy
function delayMiliseconds( milliseconds: number = 0 ) { return function ( target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; descriptor.value = function (...args) { setTimeout(() => { originalMethod.call(this, ...args); }, milliseconds); }; return descriptor; }; }

Decorator delayMiliseconds take one parameter milliseconds (a default value of the milliseconds would be 0). This parameter would be used in setTimeout function as an amount of the delay.

As you can see above, delayMiliseconds takes three parameters:

  • target -  either the constructor function of the class for a static member or the prototype of the class for an instance member. In our example, Notification.prototype would be a target.
  • propertyKey - the method name that is being decorated.
  • PropertyDescriptor - describes a property on an [Object]:
<>Copy
interface PropertyDescriptor { configurable?: boolean; enumerable?: boolean; value?: any; writable?: boolean; get? (): any; set? (v: any): void; }

In case if we want to change the behavior of the method, we should redefine value in the property descriptor with a new functionality:

<>Copy
const originalMethod = descriptor.value; // a reference to the original descriptor.value = function (...args) { setTimeout(() => { originalMethod.call(this, ...args); // bind a context of Notification }, milliseconds); }; return descriptor; // return descriptor with a new behaviour

To apply the decorator to our method notifyUser  we should use special syntax - prefixed symbol @.

<>Copy
class DelayedNotification { type: string; constructor(type) { this.type = type } @delayMiliseconds(300) notifyUser() { console.log(`${this.type} notification`); } }

Let's display the instance of DelayedNotification as we did earlier in the console:

console.log(new DelayedNotification('Success'))

Content imageContent image

From this screenshot, we can observe that DelayedNotification instance does not have a method notifyUser.  NotifyUser method exists only in DelayedNotification prototype. It happens because delayMiliseconds is supposed to work with prototype methods because it relies on descriptor.

There is additional proof for that - let`s run  the same  verification in console, as we provide earlier in the functional approach:

console.log('notifyUser' in notification);   // true

console.log(Object.getPrototypeOf(notification).hasOwnProperty('notifyUser')) ;  // true

As a result, you see true in console as in the functional approach because notification prototype has a method notifyUser.

As you can see, that's one of the substantial differences between functional and class-based approaches is that we have decorated the method on a prototype in a class-based approach and have not in a functional approach.

Factory pattern with classes aren't as popular as with the functional approach because you can directly instantiate an object through its class. But if we wanted to do that, there's nothing special we should do. The decorator will applied automatically when JS will be creating a class instance. Let`s create a class-based factory:

<>Copy
// classBased factory function classBasedObjectAFactory() { return new Notification('Success'); } const notification = classBasedObjectAFactory(); notification.notifyUser(); // 'Success notification' in console after 3 seconds

As you can see from the screenshot above, a factory function provides the ability to produce an object by creating a class instance with a new keyword.


5. Where you can find usage of Decorators?

You can find of usage of Decorators in the next frameworks:

  • Angular - @Component, @Directive, @Injectable, @Pipe

To learn more about the component decorator mechanism in Angular you can read this article.

  • Mobx -  @observable, @computed и @action
  • React - HoC (for create higher-order components)
  • Core-decorators library - @**readonly ,@**extendDescriptor, @**override,** @autobind

Explanation of how Angular @Injectable decorator works under the hood in Angular Ivy you can read here

Conclusion

Decorator is a function that gives the ability to dynamically modify the existing functionality:

  • by higher-order functions in the functional implementation;
  • by modifying functionality using method/property decorators in the class-based implementation.

The main purpose of using decorators - dynamically add logic to the objects, compose behavior without modifying the original function.

You can use decorators in the next cases:

  • you want to add new functionality to the object (this functionality could be easily deleted from this object later)
  • replace an inheritance of classes by creating a decorators

There are some advantages and disadvantages of using decorators in Javascript:

Advantages:

  • reusability - the main advantage of the decorator is that you can apply the same logic to a different method without rewriting them.
  • improve readability and extensibility;
  • decrease amount of code;
  • implementation of the Decorator pattern.

Disadvantages:

  • The decorator design pattern creates lots of similar decorators which are sometimes hard to maintain.
  • With multiple decorators concreate subclasses and code and architecture can be complex.
  • It’s hard to remove a specific wrapper from the wrapper's stack.

Comments (1)

authorajuni880
22 December 2021

Hi, good article!

In this snippet you are missing some keywords.

function functionBasedNotificationFactory() {
	type: 'Success',

	 notifyUser() {
		console.log(`${type} notification`);
	}

	return {
		name: 'Success',
		notifyUser: delayMiliseconds(notifyUser, 300);
	}
}
function functionBasedNotificationFactory() {
	const type = 'Success'

	function notifyUser() {
		console.log(`${type} notification`);
	}

	return {
		name: 'Success',
		notifyUser: delayMiliseconds(notifyUser, 300)
	}
}
authorVarvaraSandakova
22 December 2021

@ajuni880 thanks for your comment. I fixed it.

Share

About the author

author_image

About the author

author_image
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

blockchainpost
19 July 202212 min read
An Introduction to Blockchain

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

blockchainpost
19 July 202212 min read
An Introduction to Blockchain

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

Read more
blockchainpostAn Introduction to Blockchain

19 July 2022

12 min read

Learn the fundamentals of a blockchain starting from first principles. We'll cover hashing, mining, consensus and more. After reading this article, you'll have a solid foundation upon which to explore platforms like Ethereum and Solana.

Read more