When ‘Not’ to Use Arrow Functions

It’s nice to see the programming language I use evolve from version to version, learning from mistakes, finding better implementations, and creating new features.

This is how JavaScript has evolved over the years, and it’s great when ECMAScript6 takes the language to a new level of usability: arrow functions, classes, and more!

One of the most useful features is the arrow function, and there are many excellent articles describing its context transparency and short syntax that you should read if you are not familiar with ES6.

But there are two sides to every medal. Often new features cause some confusion, one of which is the misuse of arrow functions.

This article looks at some scenarios where you should bypass the arrow function in favor of old function expressions or newer shorthand syntax, and shorten it to avoid compromising the readability of your code.

1. Define methods on objects

In JavaScript, a method is a function stored on an object property, and when a method is called, this is the object to which the method belongs.

1A. Object literals

Because of the short syntax of the arrow function, it is tempting to use it in method definitions. Here’s a try:

const calculate = { array: [1, 2, 3], sum: () => { console.log(this === window); / / = >true
    returnthis.array.reduce((result, item) => result + item); }}; console.log(this === window); / / = >true
// Throws "Type exception: cannot read attribute 'reduce' of undefined"
calculate.sum();
Copy the code

Define the calculate.sum method through the arrow function, but calling calculate.sum() throws a type exception because this.array is equal to undefined. When the sum method on the Calculate object is called, the context is still Window because the context is lexically bound to the Window object. Executing this.array is equivalent to executing the value window.array, which is undefined.

The solution is to use function expressions or short syntax for function definitions (for ECMAScript6). In this case, this depends on the caller rather than the enclosing context. Look at the modified code:

const calculate = {  
  array: [1, 2, 3],
  sum() { console.log(this === calculate); / / = >true
    returnthis.array.reduce((result, item) => result + item); }}; calculate.sum(); / / = > 6Copy the code

Because sum is a regular function, the this in the calculate.sum call is the calculate object and this.array is an array reference, so the correct sum of the elements evaluates to: 6.

1b. Object prototype

The same rule applies to prototype objects. If you define the sayCatName method with the arrow function, you get the incorrect context Window.

functionMyCat(name) { this.catName = name; } MyCat.prototype.sayCatName = () => { console.log(this === window); / / = >true
  return this.catName;
};
const cat = new MyCat('Mew');
cat.sayCatName(); // => undefined
Copy the code

Use old-fashioned function expressions:

function MyCat(name) {
  this.catName = name;
}
MyCat.prototype.sayCatName = function() { console.log(this === cat); / / = >true
  return this.catName;
};
const cat = new MyCat('Mew'); cat.sayCatName(); / / = >'Mew'
Copy the code

When sayCatName is called as a method: cat.saycatName (), it changes the context to the cat object.

2. Callback functions via dynamic context

This is a great feature of JavaScript that allows you to change the context based on how a function is called. Usually, the context is the target object where the call takes place, which makes the code more natural, as if “something is happening on this object”.

However, the arrow function is statically bound to the context when it is declared, making it impossible to become dynamic. In this case, that’s the flip side of the medal, the word “this” is no longer necessary.

Binding event listeners to DOM elements is a common task in client-side programming, and events trigger handlers with this as the target element, making dynamic contexts easier to use.

The following example uses the arrow function as a handler:

const button = document.getElementById('myButton');
button.addEventListener('click', () => { console.log(this === window); / / = >true
  this.innerHTML = 'Clicked button';
});
Copy the code

This is the window in the arrow function defined in the global context. When the click event is triggered, the browser tries to call the handler in the Button context, but the arrow function does not change its predefined context. This.innerhtml is equivalent to window.innerhtml. It doesn’t make sense.

You must apply the function expression, which allows you to change this based on the target element:

const button = document.getElementById('myButton');
button.addEventListener('click'.function() { console.log(this === button); / / = >true
  this.innerHTML = 'Clicked button';
});
Copy the code

When the user clicks on a button, this is a button in the handler, so this.innerHTML = ‘Clicked button’ correctly modifies the text of the button to reflect the Clicked state.

3. Call the constructor

In the constructor call this is the newly created object, and when new MyFunction() is called, the context of the constructor MyFunction is the new object: this instanceof MyFunction === true.

Note that arrow functions cannot be used as constructors, and JavaScript explicitly prevents this from happening by throwing exceptions. In any case, this is set from a closed context, not a newly created object. That is, the arrow function constructor call is meaningless and ambiguous.

Let’s see what happens if we do this:

const Message = (text) => { this.text = text; }; // throw "type exception: Message is not a constructor const helloMessage = new Message('Hello World! ');
Copy the code

Execute new Message(‘Hello World! ‘), Message is the arrow function, JavaScript throws a type exception: Message cannot be used in constructors.

In this case, ECMAScript 6 gives you a long error message, which I think is effective compared to the previous JavaScript versions.

The above example can be corrected by using function expressions, which is the correct way (including function declarations) to create constructors:

const Message = function(text) {
  this.text = text;
};
const helloMessage = new Message('Hello World! '); console.log(helloMessage.text); / / = >'Hello World! '
Copy the code

Too short syntax

The nice feature of the arrow function is that it can omit the parentheses of arguments, the curly braces of the block {}, and the return(if the function body has only one statement), which allows us to write very short functions.

My college programming professor gave his students an interesting task: Write a shortest function in C to calculate the length of a string, which is a great way to learn and explore a new language.

In practice, however, the code will be read by many developers, and the shortest syntax is sometimes not suitable for your colleagues to understand functions quickly.

To some extent, compressing a function makes it hard to read, so try not to use it on a whim. Here’s an example:

const multiply = (a, b) => b === undefined ? b => a * b : a * b; const double = multiply(2); double(3); // => 6 multiply(2, 3); / / = > 6Copy the code

Multiply returns the result of a multiplication of two numbers, or a closure bound to the first argument (for subsequent multiplications). The function executes correctly and is short, but not easy to understand at first glance.

To improve readability, you can restore the optional curly braces and return statements from the arrow function, or use the regular function:

function multiply(a, b) {
  if (b === undefined) {
    return function(b) {
      returna * b; }}returna * b; } const double = multiply(2); double(3); // => 6 multiply(2, 3); / / = > 6Copy the code

It’s best to strike a balance between brevity and verbosity to make your JavaScript more intuitive.

5. To summarize

There is no doubt that the arrow function is a great feature. If used correctly, it simplifies where you had to use.bind() or try to capture context earlier, and it also simplifies code.

There are situations where advantages are a disadvantage in other places. Arrow functions cannot be used when dynamic context is required: methods are defined, objects are created using constructors, and targets are retrieved from this when handling events.