The advent of ES6
brought us many exciting features, and among them, arrow functions stood out as a concise and elegant way to write functions. However, while arrow functions have been embraced for their brevity, they come with certain quirks that can be a double-edged sword for developers.
Table of Contents
The Anonymous Nature of Arrow Functions
The Inferred Purpose
The Power of Descriptive Function Names
The Readability Factor
Arrow Functions in the Context of this
The Essence of Lexical this
Arrow Functions and the new Keyword
The Curly Brace Confusion
The Parent Lexical Scope
The Right Tool for Lexical this Behavior
Conclusion
Sources
The Anonymous Nature of Arrow Functions
Arrow functions are inherently anonymous, unlike traditional functions with a distinct name. This anonymity can impact code readability, making it challenging for developers to quickly discern the purpose of a function. Consider this arrow function:
const getId = people.map(person => person.id);
As Kyle Simpson aptly put it, **"The only way to figure out what an arrow function is doing is to read its function body." **This lack of a name can obscure the function's intention, leading to potential confusion for you and other developers working on the code.
The Inferred Purpose
Proponents of arrow functions argue that their purpose is self-evident. However, it's essential to remember that the purpose of an arrow function is inferred rather than explicitly expressed through a well-chosen name. You are required to deduce the function's intent by examining its code, which can sometimes lead to ambiguity.
The Power of Descriptive Function Names
In contrast, when you use a named function, you provide a clear and explicit label for the function's purpose. For example, consider the alternative using a function declaration:
function getIdFromPerson(person) {
return person.id;
}
const ids = people.map(getIdFromPerson);
In this revised code, the function getIdFromPerson
conveys its purpose without ambiguity. It's evident that this function retrieves the id
property from a person
object. Descriptive names enhance code readability and maintainability, making it easier for you and your team to understand and work with the codebase.
The Readability Factor
While arrow functions offer brevity, they can sometimes compromise readability. Self-explanatory code, where the purpose of a function is evident from its name, often proves invaluable. Arrow functions' shorter syntax may initially appear more straightforward, but the brevity can come at the cost of understanding and maintainability. Moreover, arrow functions come in various syntax variations, leading to potential confusion in certain contexts.
Arrow Functions in the Context of this
However, it's crucial to acknowledge that arrow functions bring a unique characteristic called "lexical this behaviour." This behaviour can be immensely valuable in specific situations.
Consider this example:
const workshop = {
topic: "JavaScript",
ask: function () {
setTimeout(() => {
console.log(`Welcome to the ${this.topic} workshop!`);
}, 1000);
},
};
workshop.ask(); // Outputs: Welcome to the JavaScript workshop!
In this case, the ask
method of the workshop object contains an arrow function passed to setTimeout
. Surprisingly, within the arrow function, the this
keyword correctly points to the workshop
object. This phenomenon is the essence of "lexical this behavior."
Now, let's dive deeper into what "lexical this" means:
An arrow function does not define a this
keyword. In fact, it doesn't have a this
keyword at all; it treats this like any other variable.
The Essence of Lexical this
In essence, lexical this means that an arrow function will keep climbing up the scope chain until it locates a function with a defined this
keyword. The this
keyword in the arrow function is determined by the function containing it. In the example, it looks up one level in scope and finds the ask function.
This understanding is crucial because it helps you avoid incorrect thinking, which can lead to bugs in your code. By thinking in alignment with JavaScript's design and the language specification (the spec), you can ensure that your code behaves as expected.
The JavaScript language specification confirms that an arrow function does not define local bindings for
arguments
,super
,this
, ornew.target
. This specification is a key point of reference, and adhering to it eliminates misconceptions and potential issues in your code.
Arrow Functions and the new Keyword
One fascinating aspect of arrow functions is their behavior concerning the new
keyword. If you recall from earlier in this series, the new
keyword takes precedence over a hardbound function. That means, for some unusual reason, if you call new
on a hardbound function, it can override the hard binding and become the new object
. However, this is not the case with arrow functions. Calling new
on an arrow function results in an exception, as arrow functions are not hardbound functions.
"TypeError: function is not a constructor"
Understanding this distinction is vital for maintaining code clarity and preventing unexpected behavior in edge cases.
The Curly Brace Confusion
One of the persistent frustrations among developers is the assumption that curly braces {}
imply a scope. We often associate them with blocks
, function
bodies, or scopes. However, it's essential to understand that not every set of curly braces denotes a scope.
Consider this example:
const workshop = {
teacher: "Kyle",
ask: (question) => {
console.log(this.teacher, question}
},
};
At first glance, it appears that this function is enclosed in a scope. Still, this is not the case. These curly braces merely define the function body; they don't create a scope. Arrow functions don't introduce a new scope like regular functions do.
The Parent Lexical Scope
One common misconception is that arrow functions should inherit the scope in which they are defined. For example, you might expect an arrow function defined within an object to capture that object as its this
context. However, this isn't the case.
Arrow functions are not context-aware; they don't possess their own this
. Instead, they resolve this
lexically, which means they inherit the this
from their parent lexical scope. In the global scope, this would typically be the global object.
For example, in the workshop object:
workshop.ask("What happened to 'this'?")
// undefined What happened to 'this'?
The this
inside the arrow function does not point to the workshop
object but inherits the this
from the global scope. So, it logs undefined
.
The Right Tool for Lexical this Behavior
Arrow functions are not a one-size-fits-all solution; they are the right tool for maintaining the this
context from their parent scope. For instance, when you need to pass a function as a callback to methods like setTimeout
, using an arrow function ensures that this
remains consistent with the context
in which it's defined.
Let's consider a scenario:
workshop.ask.call(workshop, "Still no 'this'");
// undefined Still no 'this'
Here, we explicitly use call
to set the this
context to the workshop
object. However, the arrow function still resolves this
lexically, ignoring the provided context. It logs undefined
.
Conclusion
The key takeaway here is that arrow functions are not a one-size-fits-all solution. While they excel in certain situations, such as when you require lexical this behaviour, they may not be the best choice for every use case. It's crucial to understand their behaviour and limitations fully. By doing so, you can wield arrow functions effectively, leveraging their strengths while avoiding their potential pitfalls. Ultimately, mastering this powerful feature will help you become a more proficient and versatile JavaScript developer.
Sources
Kyle Simpson's "You Don't Know JS"
MDN Web Docs - The Mozilla Developer Network (MDN)
Top comments (0)