• JavaScript Decorators From Scratch
  • By Mahdhi Rezvi
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: regon – cao
  • Proofread by zenblo

What is a decorator?

A decorator is simply a function that wraps another function to extend its existing functionality. You can use another piece of code to “decorate” the existing code. This concept is familiar to those familiar with function combinations or higher-order functions.

Decorators are nothing new. It has appeared in other languages such as Python and even in JavaScript functional programming. We’ll talk about that later.

Why decorators?

Decorators let you write cleaner code and achieve composition. It also helps you extend the same functionality to multiple functions and classes, enabling you to write code that is easier to debug and maintain.

Decorators also reduce code interference because they remove all enhanced code from core functions. It also enables you to add new features without increasing code complexity.

In the phase 2 proposal, there may be many proposals that would be useful for class decorators.

Function decorator

What is a function decorator?

Function decorators are simple functions. They take an old function as an argument and return another new function that enhances and extends the arguments of the old function. The new function does not modify the original function parameters, but instead uses the original function parameters in its own function body. This is very similar to the higher-order functions mentioned earlier.

How does a function decorator work?

Let’s look at an example to understand the function decorator.

Parameter validation is common in programming. In a language like Java, if a function requires two arguments and three arguments are passed, an exception is received. But in JavaScript, you don’t get any errors because the extra arguments are simply ignored. This behaviour can be irritating and useful.

To ensure that the arguments passed to the function are valid, we can verify them at the entry. This is a simple procedure that allows you to check that each parameter has the required data type and ensure that the number of parameters does not exceed the required number of parameters.

But repeating the same procedure for several functions can lead to code duplication. You can simply use a decorator to help you with validation and reuse it where parameter validation is needed.

// The decorator function
const allArgsValid = function(fn) {
  return function(. args) {
  if(args.length ! = fn.length) {throw new Error('Only submit required number of params');
    }
    const validArgs = args.filter(arg= > Number.isInteger(arg));
    if (validArgs.length < fn.length) {
      throw new TypeError('Argument cannot be a non-integer');
    }
    return fn(...args);
  }
}

// The normal multiplication function
let multiply = function(a,b){
	return a*b;
}

// The decorator multiplication function accepts only a specified number of integer arguments
multiply = allArgsValid(multiply);

multiply(6.8);
/ / 48

multiply(6.8.7);
//Error: Only submit required number of params

multiply(3.null);
//TypeError: Argument cannot be a non-integer

multiply(' '.4);
//TypeError: Argument cannot be a non-integer
Copy the code

AllArgsValid is a decorator function that takes a function as an argument. This decorator function returns another function that encapsulates the function’s arguments. Furthermore, it calls the argument function when the argument to the function passed in is a valid integer. Otherwise, an error is thrown. It also checks the number of arguments passed and ensures that it does not exceed the required number.

We then assign a function that multiplicates two numbers to a variable named multiply. We pass this multiplication function to allArgsValid and assign the returned new function again to the multiply variable. This makes it easier to reuse when needed.

// Ordinary addition function
let add = function (a, b) {
  return a + b;
};

// Decorates the addition function, which accepts only a specified number of integer arguments
add = allArgsValid(add);

add(6.8);
/ / 14

add(3.null);
//TypeError: Argument cannot be a non-integer

add("".4);
//TypeError: Argument cannot be a non-integer
Copy the code

TC39 class decorator proposal

In the world of JavaScript functional programming, function decorators have been around for a long time. The class decorator proposal is currently in phase 2.

JavaScript classes aren’t really classes; they’re just syntactic sugar for the archetypal pattern. It’s just that the class syntax makes it easier for developers to use.

We can now conclude that classes are simple functions. You may now be wondering, why can’t we simply use function decorators in our classes? Absolutely.

Let’s look at an example to see how this can be done.

function log(fn) {
  return function () {
    console.log("Execution of " + fn.name);
    console.time("fn");
    let val = fn();
    console.timeEnd("fn");
    return val;
  };
}

class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return ` [The ${this.name}] [The ${this.ISBN}] `; }}let obj = new Book("HP"."1245-533552");
let getBook = log(obj.getBook);
console.log(getBook());
//TypeError: Cannot read property 'name' of undefined
Copy the code

The reason for the above error is that when the getBook method is called, it actually calls the anonymous function returned by the log decorator function. Obj. getBook is called in this anonymous function, but the this value in the anonymous function refers to the global object, not the book object. So, we get type errors.

We can solve this problem by passing an instance of the Book object to the getBook method.

function log(classObj, fn) {
  return function () {
    console.log("Execution of " + fn.name);
    console.time("fn");
    let val = fn.call(classObj);
    console.timeEnd("fn");
    return val;
  };
}

class Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return ` [The ${this.name}] [The ${this.ISBN}] `; }}let obj = new Book("HP"."1245-533552");
let getBook = log(obj, obj.getBook);
console.log(getBook());
//[HP][1245-533552]
Copy the code

We pass bookObj to the log decorator function, which passes to the obj.getBook method as this.

This approach solves the problem, but it seems like an alternative. In the new proposal, the decorator syntax can be more reasonable and efficient to solve our problem.

Note — Babel is required to run the following example. Try these examples online Jsfiddle is a good choice. Since these proposals have not been finalized, you should avoid using them in a production environment, as they may change in the future and the current performance is not perfect.

Class decorator

In the new proposal the decorator uses a special syntax prefixed with the @ symbol. We will use the new syntax to invoke the log decorator.

@log
Copy the code

Some changes have been made to the decorators in the proposal. When a decorator is used on a class, it receives a target argument, and tagrget is the object instance of the class being decorated.

With access to the Target parameter, you can modify the class’s constructor as needed, add new stereotype properties, and so on.

Let’s take a look at the Book example we used earlier.

function log(target) {
  return function (. args) {
    console.log("Constructor called");
    return newtarget(... args); }; } @logclass Book {
  constructor(name, ISBN) {
    this.name = name;
    this.ISBN = ISBN;
  }

  getBook() {
    return ` [The ${this.name}] [The ${this.ISBN}] `; }}let obj = new Book("HP"."1245-533552");
/ / call the Constructor
console.log(obj.getBook());
//HP][1245-533552]
Copy the code

As shown above, the log decorator takes the target argument and returns an anonymous function that executes the log statement to create and return an instance of the Book class. You can target. The prototype. The property in the target to add a new prototype properties.

You can even use multiple decorators on a single class.

function logWithParams(. params) {
  return function (target) {
    return function (. args) {
      console.table(params);
      return newtarget(... args); }; }; } @log @logWithParams("param1"."param2")
class Book {
  // Same code as before
}

let obj = new Book("HP"."1245-533552");
/ / call the Constructor
// The arguments are printed as tables
console.log(obj.getBook());
//[HP][1245-533552]
Copy the code

Class property decorator

Class attribute decorators have the same syntax as class decorators, prefixed with “@”. You can also pass parameters to the decorator as attributes of the class.

Class method decorator

The parameters passed to the class method decorator are different from those of the class decorator. The class method decorator accepts three arguments instead of one. Details are as follows:

  • Target – An object that contains the constructor and method of the class.
  • Name – The Name of the method being invoked.
  • Descriptor – Description object of the method being called. You can learn more about attribute descriptors here.

The descriptor object for a class method has the following four properties, which will suffice most of the time.

  • A 6464x – specifies whether the property descriptor can be modified without any additional control.
  • Enumerable – Determines whether an object is visible when Enumerable, a Boolean value.
  • Value – The Value of an attribute. This is pointing to a function.
  • Writable – Boolean value that determines whether a property can be overridden.

Let’s look at the example of the Book class again.

// Read only decorator functions
function readOnly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class Book {
  // call from here
  @readOnly
  getBook() {
    return ` [The ${this.name}] [The ${this.ISBN}] `; }}let obj = new Book("HP"."1245-533552");

obj.getBook = "Hello";

console.log(obj.getBook());
//[HP][1245-533552]
Copy the code

The readOnly decorator for the above example makes the getBook method in the Book class read-only by setting the writable property of the descriptor to false. This property defaults to true.

If the Writable property is not manipulated, the getBook property can be easily overridden as follows:

obj.getBook = "Hello";

console.log(obj.getBook);
//Hello
Copy the code

Class field decorator

Like class methods, class fields can be decorated. Typescript already supports class fields, but they are still in JavaScript’s phase 3 proposal.

The class field decorator accepts the same parameters as the class method decorator, the only difference being the descriptor object. Unlike class methods, descriptor objects do not include the value attribute when used on class fields. Instead, they are replaced with a function called initializer. Since the class field is still in the proposal stage, you can read more about Initializer in the documentation. The Initializer function returns the initial value of the class field variable.

In addition, when the field value is undefined, the writable property of the descriptor object does not exist.

Let’s look at an example to further understand this point. We’ll use our Book class again.

function upperCase(target, name, descriptor) {
  if (descriptor.initializer && descriptor.initializer()) {
    let val = descriptor.initializer();
    descriptor.initializer = function () {
      returnval.toUpperCase(); }; }}class Book {
  @upperCase
  id = "az092b";

  getId() {
    return `The ${this.id}`;
  }

  // Other code
}

let obj = new Book("HP"."1245-533552");

console.log(obj.getId());
//AZ092B
Copy the code

The above example converts the value of the ID attribute to uppercase. It uses the upperCase decorator, which checks if initializer exists to make sure it is not undefined, checks if it is true, and then converts it to upperCase. The getId method is called to see the values in uppercase. You can also pass arguments to a decorator when using it on a class field.

Use case

There are many use cases for decorators. Here are a few examples in practice.

Use decorators in Angular

If anyone is familiar with typescript and Angular, they will surely encounter decorators used in Angular classes. You’ll find things like @Component, @NgModule, @Injectable, @pipe, etc. These built-in decorators are used to decorate classes.

MobX

MobX promoted and used “@Observable,” “@computed,” and “@Action” decorators before version 6. MobX does not currently encourage the use of decorators because proposals are not standardized. The document states as follows:

However, decorators are not currently an ES standard, and the standardization process will continue for a long time. In addition, the decorator standard may be implemented differently than before.

Core Decorators JS

This JavaScript library provides ready-made decorators. Although this library is based on the phase 0 decorator proposal, the authors of the library will wait until the phase 3 proposal to update the library.

This library comes with decorators such as “@readonly”, “@time”, and “@deprecate”. You can learn more here.

Redux Library in React

The React Redux library includes a connect method that allows you to connect React components to the Redux repository. The library also allows the CONNECT method to be used as a decorator.

// Before using the decorator
class MyApp extends React.Component {
  / /... Define your own application
}
export default connect(mapStateToProps, mapDispatchToProps)(MyApp);

// After using the decorator
@connect(mapStateToProps, mapDispatchToProps)
export default class MyApp extends React.Component {
  / /... Define your own application
}
Copy the code

Felix Kling’s Stack Overflow answer explains this.

In addition, although Connect supports decorator syntax, the Redux team discourages it, mainly because decorators in the Phase 2 proposal may change in the future.


In summary, decorators are a powerful tool that allows you to write flexible code. You’ll be encountering it a lot in the near future.

Thanks for reading and happy coding!

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.