ECMAScript.next: Arrow Functions and Method Definitions
Join the DZone community and get the full member experience.
Join For FreeTerminology
For this blog post, we distinguish two kinds of callable entities:
- A subroutine exists on its own and is called directly. Subroutines are also often called “non-method functions” in JavaScript.
- A method is part of an object o and called via an object (which isn’t necessarily the same object as o).
In JavaScript, both subroutines and methods are implemented by functions. For example:
var obj = { myMethod: function () { setTimeout(function () { ... }, 0); } }
myMethod is a method, the first argument of setTimeout() is a subroutine. Both are implemented by functions. Before we can explain how functions work as methods, we need to make a few more definitions.
Whenever a function is called, it is in two kinds of scopes (or contexts): Its lexical scopes are the syntactic constructs that surround it (a trait of the source code or the lexicon). Its dynamic scopes are the function that called it, the function that called that function, etc. Note the nesting that occurs in both cases. Free variables are variables that aren’t declared within a function. If such variables are read or written, JavaScript looks for them in the surrounding lexical scopes. Functions work well as method implementations: They have a special variable called this that refers to the object via which the method has been invoked. In contrast to other free variables, this isn’t looked up in the surrounding lexical scopes, it is handed to the function via the invocation. As the function receives this dynamically, it is called dynamic this.
Functions don’t work well as implementations of subroutines, because this is still dynamic. The subroutine call sets it to undefined in strict mode [1] and to the global object, otherwise. That means that it shadows the this of a surrounding method, even though the function-as-subroutine has no use for it, whatsoever. For example:
var jane = { name: "Jane", logHello: function (friends) { var that = this; // (*) friends.forEach(function (friend) { console.log(that.name + " says hello to " + friend) }); } }
The argument of forEach is a subroutine. You need the assignment at (*) so that it can access logHello’s this. Clearly, subroutines should have lexical this, meaning that this should be treated the same as other free variables and looked up in the enclosing lexical scopes. that = this is a good work-around. It simulates lexical this, if you will. Another work-around is to use bind:
var jane = { name: "Jane", logHello: function (friends) { friends.forEach(function (friend) { console.log(this.name + " says hello to " + friend) }.bind(this)); } }
Now the argument of forEach has a fixed value for this. I can’t be changed, not even via call or apply. There are two problems with any simulation of lexical this:
- You have to know how JavaScript’s quirky this works (which you should neither want to nor need to).
- You have to constantly be alert as to when to simulate lexical this. That choice should be automatic and not require extra thought. Note that even with CoffeeScript’s fairly elegant arrow functions, that problem doesn’t completely go away.
Arrow functions
ECMAScript.next’s arrow functions are better suited for defining subroutines than normal functions, because they have lexical this. Using one for forEach looks as follows.
let jane = { name: "Jane", logHello: function (friends) { friends.forEach(friend => { console.log(this.name + " says hello to " + friend) }); } }
The “fat” arrow => (as opposed to the thin arrow ->) was chosen to be compatible with CoffeeScript, whose fat-arrow functions are very similar.
Specifying arguments:
() => { ... } // no argument x => { ... } // one argument (x, y) => { ... } // several argumentsSpecifying a body:
x => { return x * x } // block x => x * x // expression, equivalent to previous lineThe statement block behaves like a normal function body. For example, you need return to give back a value. With an expression body, the expression is always implicitly returned. Note how much an arrow function with an expression body can reduce verbosity: The example given above saves you from typing
function (x) { return x * x }
Implementing lexical this
For arrow functions, lexical this is implemented as follows. The arrow function
x => x + this.yis mostly syntactic sugar for
function (x) { return x + this.y }.bind(this)That expression creates two functions: First, the original anonymous function with the parameter x and dynamic this. Second, the bound function that is the result of bind. While an arrow function behaves as if it had been created via bind, it consumes less memory: Only a single entity is created, a specialized function where this is directly bound to the this of the surrounding function.
Arrow functions versus normal functions
An arrow function is different from a normal function in only two ways: First, it always has a bound this.
Second, it can’t be used as a constructor: There is no internal method
[[Construct]] (that allows a normal function to be invoked via new) and no property prototype. Therefore, new (() => {}) throws an error.
Apart from these limitations, there is no observable difference between an arrow function and a normal function. For example, typeof and instanceof can be used as before:
> typeof () => {} 'function' > () => {} instanceof Function true
Syntactic variants under discussion
The following syntactic variants are still being discussed and might not be added to ECMAScript.next.
- Omitting the parameters:
=> { ... }
With JavaScript’s automatic semicolon insertion [2], there is a risk of such an expression being wrongly considered as continuing a previous line. Take, for example, the following code.var x = 3 + a => 5
These two lines are interpreted asvar x = 3 + (a => 5);
However, arrow functions will usually appear in expression context, nested inside a statement. Hence, I wouldn’t expect semicolon insertion to be much of a problem. If JavaScript had significant newlines [3] (like CoffeeScript) then the problem would go away completely. - Omitting the body:
x =>
That’s a function with a single parameter that always returns undefined. It is a synonym for the void operator [4]. I’m not sure how useful that is. - Named arrow functions: JavaScript already has named function expressions, where you give a function a name so that it can invoke itself. That name is local to that function, it doesn’t leak into any surrounding scopes. Named arrow functions would work the same. For example:
let fac = me(n) => { if (n <= 0) { return 1; } else { return n * me(n-1); } } console.log(me); // ReferenceError: me is not defined
Parsing arrow functions
Most JavaScript parsers have a two-token look-ahead. How then is such a
parser supposed to distinguish between the following two expressions?
(x, y, z) (x, y, z) => {}The first expression is the comma operator applied to three variables, in parentheses. The second expression is an arrow function. To parse both of the above, one uses a trick called cover grammar: One creates a grammar rule that covers both use cases, parses and then performs post-processing. If an arrow follows the closing parenthesis, some previously parsed things will raise an error and the parsed construct is used as the formal parameter list of an arrow function. If no arrow follows, other previously parsed things will raise an error and the parsed construct is an expression in parentheses. Some things can only be done in a parenthesized expression:
(foo(123))Other things can only be done in a parameter list. For example, declaring a rest parameter:
(a, b, ...rest)Several things can be done in both contexts:
(title = "no title")The above is an assignment in expression context and a declaration of a default parameter value in arrow function context.
Possible arrow function feature: optional dynamic this
One arrow function feature, that has been deferred and might still be added, is the ability to switch to dynamic this. The use case for that feature is as follows. In jQuery, some arguments are subroutines that have this as an implicit parameter:
$(".someCssClass").each(function (i) { console.log(this) });
You currently cannot write each’s argument as an arrow function, because call and apply cannot override the arrow function’s bound value for this. You would need to switch to dynamic this to do so:
$(".someCssClass").each((this, i) => { console.log(this) });
Normally, no parameter can have the name this, so the above is a good marker for an arrow function with dynamic this.
Two approaches that don’t work
Shouldn’t there be a simpler solution for optional dynamic this? Alas, two seemingly simpler approaches won’t work.
Possible approach #1: Change between dynamic and lexical this, depending on how a function is invoked. If it is invoked as a method, use dynamic this. If it is invoked as a subroutine, use lexical this. The problem with this approach is that you can’t control how a function you write will be used by clients, opening the door to inadvertently exposed secrets and unintended side effects. Example: Let’s pretend there are “thin arrow functions” (defined via ->) that switch between dynamic and lexical this, on the fly.
let objectCreator = { create: function (secret) { return { secret: secret, getSecret: () -> { return this.secret; } }; }, secret: "abc" }
This is how one would normally use obj:
let obj = objectCreator.create("xyz"); // dynamic this: console.log(obj.getSecret()); // xyz
This is how an attacker could get access to objectCreator.secret:
let obj = objectCreator.create("xyz"); let func = obj.getSecret; // lexical this: console.log(func()); // abc
Possible approach #2: Let call or apply override the bound value of this. That is problematic, because call or apply can accidentally break an arrow function that relies on this being lexical. Hence, this is too brittle a solution.
Arguing in favor of simplicity
I don’t think that we need optional dynamic this for arrow functions, it partially destroys their simplicity. Furthermore, having dynamic this available in something that is not a method goes against the grain of object-orientation. Every time an API hands an argument to a subroutine via this, it should instead introduce a real parameter. jQuery already allows you to do that for the each method:
Opinions expressed by DZone contributors are their own.
Trending
-
Avoiding Pitfalls With Java Optional: Common Mistakes and How To Fix Them [Video]
-
From On-Prem to SaaS
-
Essential Architecture Framework: In the World of Overengineering, Being Essential Is the Answer
-
Implementing a Serverless DevOps Pipeline With AWS Lambda and CodePipeline
Comments