Limitations of Arrow Functions

Due to their concise and expressive nature, we’ll be tempted to use arrow functions rather than anonymous functions. However, the choice should not be due to infatuation. Learning about the limitations of arrow functions will help us make an educated choice between anonymous functions and arrow functions.

Only Anonymous

Named anonymous function sounds like an oxymoron, but a function created in the argument list of a method call may be given a name. Here’s an example of naming a function that’s created just in time in the argument list.

 setTimeout(​function​ repeat(count) {
  console.log(​'called...'​);
 if​(count > 1)
  setTimeout(repeat.bind(​null​, count - 1), 1000);
 }.bind(​null​, 5), 1000);

The function passed as an argument to the setTimeout() function has a name, repeat. This name is useful to make recursive calls to it.

A named function may be stored into a variable as well, like so:

 const​ repeat = ​function​ repeat(count) {
  console.log(​'called...'​);
 if​(count > 1)
  setTimeout(repeat.bind(​null​, count - 1), 1000);
 };
 
 setTimeout(repeat.bind(​null​, 5), 1000);

Unlike traditional functions, arrow functions can’t be named—they’re truly anonymous. Arrow functions can be stored into a variable, but we can’t give them a reliable name like we can non-arrow functions.

Not a Constructor

Functions may be intended to serve as constructors and the callers may use them with new to create instances. For example, the following function represents a class—the way we once defined classes in JavaScript.

 //function Car(year) {
 //or
 const​ Car = ​function​(year) {
 this​.year = year;
 };

Traditionally, function names were capitalized to convey they represent classes and regular functions otherwise. Regardless of how we name the functions, all are constructors. We can create an instance from this function, like so:

 const​ car1 = ​new​ Car(2018);

Arrow functions can’t serve as constructors and can’t be used to instantiate objects. Let’s examine what happens if we try to use new on an arrow function:

 const​ Car = (year) => ​this​.year = year;
 
 const​ car1 = ​new​ Car(2018);

Any attempt to run this code will end up with an error:

 const car1 = new Car(2018);
  ^
 
 TypeError: Car is not a constructor

If your intention is to create a class, anonymous functions will not serve that need. Thankfully, the alternative in modern JavaScript is not to use regular functions but to use the class keyword, as we’ll see in Chapter 7, Working with Classes.

new.target Is Lexically Scoped

Functions may use new.target to determine if they were called as a constructor or as a regular function. In the following code we use the new.target property to check how the function was called.

 const​ f1 = ​function​() {
 if​(​new​.target) {
  console.log(​'called as a constructor'​);
  }
 else​ {
  console.log(​'called as a function'​);
  }
 };
 
 new​ f1();
 f1();

If the function is called as a constructor, then new.target refers to the constructor function; otherwise it’s undefined. The output from the previous code confirms this:

 called as a constructor
 called as a function

When it comes to arrow functions, the short answer is they don’t have the new.target property. That makes sense because arrow functions can’t be invoked as a constructor. However, if we reference this property within an arrow function we will not get a “new.target is not defined” error. That may surprise you, but the reason is that property takes on lexical scope.

Here are a few examples to illustrate this behavior. Let’s first define an arrow function and call it.

 const​ arrow = () => {
  console.log(​new​.target);
 };
 
 arrow();

The arrow function is directly defined in this file. In the lexical scope, the new.target property has a value of undefined. The reference to this property within the arrow function binds to the property in the file scope. Hence we get the following output:

 undefined

If the arrow function is defined within the context of another function, it will take on the new.target property of that function. Let’s verify this with the next example.

 const​ outer = ​function​() {
 return​ () => console.log(​new​.target);
 };
 
 const​ arrow1 = ​new​ outer();
 const​ arrow2 = outer();
 
 arrow1();
 arrow2();

The outer() function returns an arrow function in response to a call. We first invoke outer() as a constructor and store the result into the variable arrow1. We then invoke outer() again, but this time as a function instead of a constructor, and store the result into the variable arrow2. Finally we call the two arrow functions, using the variables arrow1 and arrow2.

Since the first arrow function, referenced using arrow1, was created within a constructor call, its new.target property binds to the variable within the constructor. As a result, the print of this property to the console shows the reference to the constructor function. On the other hand, since the second arrow function, referenced using arrow2 was obtained from a function call, and not a constructor invocation, the new.target property is undefined. The output illustrates this scenario.

 [Function: outer]
 undefined

If you’re converting functions to arrow functions and you see references to new.target, pay attention as the semantic behavior of this property goes from a locally defined variable to lexical scoping. If the function relies on new.target, it may really be intended to be used as a constructor, at least in some context, and that may no longer be valid after converting to an arrow function.

No prototype Property

Unlike many object-oriented languages, JavaScript provides prototypal inheritance instead of class-based inheritance. Each class has a prototype that can carry common methods and properties for the instances of the class. Since classes are traditionally represented as functions, each function has a prototype property that refers to the function’s or class’s prototype. However, arrow functions don’t have the prototype property.

In the next example, we create a function and an arrow function and examine the prototype property.

 const​ aFunc = ​function​() {};
 const​ anArrow = () => {};
 
 console.log(aFunc.prototype);
 console.log(anArrow.prototype);

While the regular function, referenced by aFunc, has a valid prototype, the arrow function referenced by anArrow does not, as we see in the output:

 aFunc {}
 undefined

If an existing code makes heavy use of the prototype property of a function and injects properties and methods using prototype, it may not be a good candidate to be replaced with an arrow function.

Can’t Be Generators

In Using Generators we created a generator function that produced an infinite sequence of prime numbers. The code is repeated here for your convenience.

 const​ primesStartingFrom = ​function​*(start) {
 let​ index = start;
 
 while​(​true​) {
 if​(isPrime(index)) ​yield​ index;
  index++;
  }
 };

The variable primesStartingFrom is referring to an anonymous function, which is marked with * to indicate it is a generator. Since the function is anonymous, you may be tempted to replace the anonymous function with an arrow function, like this:

 const​ primesStartingFrom = *(start) => { ​//Will not work
 //...

Wishful thinking, but that does not work. The code will produce the following error:

 const primesStartingFrom = *(start) => { //Will not work
  ^
 
 SyntaxError: Unexpected token *

There’s no good reason for this except the support for arrow functions being generator functions has not been implemented. This may change in the future, and then it may be possible to use arrow functions as generators. Until then we have to continue to use regular or anonymous functions as generators.

throw Needs Wrapping

Single-line arrow functions are highly expressive, concise, and less noisy, and they don’t need return. For example, here’s a cute little arrow function:

 const​ alwaysTrue = () => ​true​;

The body of this arrow function returns true and there’s hardly any ceremony in that code. Sometimes we may want to throw an exception from the body of an arrow function. This is especially useful when evolving code; an exception can serve as a placeholder to remind us that a real implementation is necessary at a later time.

Here’s an arrow function that just throws an exception in its single-line body:

 const​ mapFunction = () => ​throw​ ​new​ Error(​'fail'​); ​//BROKEN CODE

That is also a concise piece of code, but it will not work; the code will produce the following when run:

 SyntaxError: Unexpected token throw

For the body of an arrow function to be a single line, it should be either a statement that returns nothing or an expression that returns some value—throw can’t be part of a single-line body. As a workaround, wrap the code snippet in {}, like so:

 const​ madFunction = () => { ​throw​ ​new​ Error(​'fail'​); };

Prefer a single-line no-noisy body over arrow functions wherever possible and switch to wrapping with {} as an exception, like when throwing exceptions.

Caution Returning Object Literals

When a language’s syntax from different contexts collide, we have to bear the pain. Let’s create a small arrow function that returns an object literal.

 const​ createObject = (name) => { firstName: name };
 
 console.log(createObject(​'George'​));

That looks benign—what could possibly go wrong? The arrow function takes name as a parameter and returns an object literal, which has a firstName property with the given name as value. Let’s run the code, in good faith, and look at the output:

 undefined

Not what we would like to see. JavaScript notices the body of the arrow function starts with { and so decides it’s a compound body instead of a single-line body. Once it steps into the body, it notices there’s no return, so it returns undefined. But what about the firstName: name—what did JavaScript do with that? JavaScript decided to treat firstName: as a label to the expression name. “But I rarely use labels,” you may protest. Well, we just did in this example, according to JavaScript.

One solution to this issue is to wrap the right side of => with a return {...}, but instead we can tell JavaScript that { is not the start of a compound body but part of an expression. To do this, use (), like so:

 const​ createObject = (name) => ({ firstName: name });
 
 console.log(createObject(​'George'​));

Let’s take a quick look at the output of running the code after this change.

 { firstName: 'George' }

When JavaScript sees ( right after =>, as opposed to {, it decides the body is a single-line expression or statement. So by placing the { after (, we are able to convey the intent in a way JavaScript understands as we meant. That’s much better—well, not really, but that’s the workaround.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.193.7