Named function expressions in JavaScript
Published on
Until today, I thought I knew all of the ways that it was possible to define a function in JavaScript.
But I jokingly tried const f = function g() {}; in a Node REPL, and it worked, so now I feel like I need to explain it to myself.
Common ways to make a function
To give some context for why I was surprised that "named function expressions" are allowed, I think it's worth showing examples of what I'm used to seeing around the JS world.
1: Function declaration
function f(a, b) {
return a + b;
}
Likely familiar to users of any other languages that descended from C.
This gives the function a name (so f.name === "f"), binds it in the scope where it was defined (so f(1, 2) === 3), and hoists the declaration within the enclosing scope (so that other functions defined at the same level can refer to it regardless of declaration order).
2: Function expressions
const f = function(a, b) {
return a + b;
};
This is almost the same thing as a function declaration: it's bound to f in the scope where it's defined, but it doesn't get hoisted, and it only has a f.name === "f" if that's trivially inferrable from syntax.
Here's a contrived example showing that the name can be empty:
const f = (x => x)(function(a, b) {
return a + b;
})
f.name === ''
(If it turns out that this is interpreter-specific, I tested this in Node v25.9.0.)
3: Arrow function expressions
const f = (a, b) => a + b;
Almost an alternate syntax for the previous one, but arrow functions don't define a new this, which makes them much nicer to use within other functions.
4: Methods in classes
class C {
f(a, b) {
return a + b;
}
}
Getting real classes saved modern JS from a lot of this confusion!
But you can still see the fact that methods are still functions underneath if you try hard enough:
(new C()).f // [Function: f]
I still try to avoid writing classes, though, because I find inheritance chains difficult to follow. I mostly included this for completeness because I have read enough of them to call them "common".
One uncommon way to define a function
const f = new Function("a", "b", "return a + b");
I have encountered this exactly once in my whole programming life. I was debugging a dependency I wasn't sure I needed, and this certainly contributed to me not wanting it anymore!
MDN warns against this, and I haven't thought of any non-cursed reasons to do this at all. (If you have one, I want to learn about it!)
This also works without the new keyword:
const f = Function("a", "b", "return a + b");
I don't know if that makes this better or worse.
A digression about how JS is full of things like this
Most practical JavaScript usage that I've encountered tends to avoid the most confusing aspects of it so that software maintenance is easier than a full rewrite.
I generally prefer === over == to avoid coercion, prefer let/const over var to avoid var hoisting 1 2, and avoid using assignments as expressions entirely.
I prefer named function declarations for everything at top-level in a file and arrow functions for inline expressions or bound to locals. They're also the most common kinds I encounter.
Some people prefer everything to be an arrow, so they avoid function declarations in the first place.
Even before arrow functions (which became part of the language in 2015 with ES6), I would sometimes see var f = function() { ... }; where authors liked all names to be bound as locals in the same way.
If you've developed a strong preference (while reading this or otherwise), consider configuring ESLint's func-style rule.
The topic of this whole ramble
const f = function g(a, b) {
return a + b;
}
f.name === 'g'
It turns out that function expressions have always been allowed to have names!
The first way I thought describe this is as a "named anonymous function expression" (which makes no sense, but I've gone with it!). Also, it really has two names! The distinction between "inner name" and "outer name" is possible to see when we introduce recursion:
const fibonacci = function fib(n) {
if (n === 0 || n === 1) return 1;
return fib(n-1) + fib(n-2);
}
fibonacci(10); // 3628800
fib(0) // ReferenceError
I don't think this has enough benefits to outweigh the confusion from a rarely-used language feature.
But if you were somehow in an environment that's trying to break your code by restricting you to exactly one mutable let binding that you only get to set the initial value expression for and you need to write a recursive function... this will let you do it.
let factorial = (n) => n == 0 ? 1 : n * factorial(n-1);
factorial(10); // 3628800
// π
const f = factorial;
factorial = () => 0;
f(10); // 0 (!!)
// π
but
let factorial = function factorial(n) {
return n == 0 ? 1 : n * factorial(n-1);
};
factorial(10); // 3628800
// π
const f = factorial;
factorial = () => 0;
f(10); // 3628800
// π You win this time!
I avoid hoisting for local variables and seek it for function definitions! It really is programmer-community expectation that decides whether that's considered "inconsistent" or "pragmatic" π€·
I like talking about hoisting, though, because it lets me bring up one of the most metal terms I've seen in programming: the temporal dead zone πΈπ€