Functions: Declarations, Expressions, and Arrows
Prerequisites
This chapter assumes you are comfortable with variables, scope, and control flow from earlier chapters, and that you know what a value is in JavaScript. We go straight into the internal differences between the three function forms and the decision criteria for choosing among them.
Three Forms, One Concept
All three syntaxes create function objects, but they differ in hoisting, this binding, arguments, and constructibility:
// Declaration: hoisted fully, has its own this and arguments, constructible
function add(a, b) { return a + b; }
// Function expression: the value is bound when that line runs
const multiply = function (a, b) { return a * b; };
// Arrow: lexical this, no arguments object, not constructible
const double = (n) => n * 2;Declarations are hoisted with their body — you can call them before the line where they appear. Function expressions hoist only the binding (const/let bindings are in the temporal dead zone until initialized), not the assignment. Arrows are always expressions.
`this` and the Arrow Exception
The decisive difference is this. Regular functions receive this from the call site; arrows close over the this of their defining scope, exactly like any other variable:
const timer = {
ticks: 0,
start() {
setInterval(() => {
this.ticks += 1; // arrow: this comes from start()
}, 1000);
},
};
timer.start();A regular function passed to setInterval would have this === undefined (strict mode) or the global object. This is the classic reason arrows exist — but it cuts both ways.
Failure Modes and Gotchas
- Arrows as methods:
const obj = { name: 'x', greet: () => this.name }losesthis. Use a method shorthand or a regular function. - Arrows as constructors:
new (arrowFn)throws aTypeError. Arrows have no[[Construct]]internal slot. - No
arguments: arrows must use rest parameters ((...args) => args.length), which is often the better API anyway. - Named vs. anonymous expressions:
const f = function helper() {}gives stack traces a name even iffis reassigned. Prefer named expressions when debugging matters. - Hoisting order: mutual recursion between function expressions fails if defined in the wrong order; declarations don't have this problem.
Object Identity and Equality
All three forms create distinct function objects. Comparing or using them as Map keys requires care:
function f() {}
const g = f;
console.log(f === g); // true — same reference
const h = () => {};
console.log(f === h); // false — different objectsDecision Criteria
| Situation | Preferred form | Why |
|---|---|---|
| Top-level utility | Declaration | Hoisting, clear name in traces |
| Callback needing `this` from enclosing scope | Arrow | Lexical `this` |
| Object/class method | Method shorthand | Dynamic `this` |
| Constructor | `class` or declaration | Arrows cannot be constructed |
| One-off inline callback | Arrow (concise body) | Minimal ceremony |
A concise arrow body ((n) => n * 2) implicitly returns an object literal only if you wrap it: () => ({ a: 1 }), not () => { a: 1 }, which is a labeled block returning undefined — a common silent bug.
const makePoint = (x, y) => ({ x, y }); // explicit parens required
console.log(makePoint(1, 2).x); // 1Exercise
Why does replacing a method shorthand with an arrow break code that reads other properties via this, and what are two fixes?
Show answer
An arrow has no dynamic this; it captures this lexically from the enclosing scope (often undefined in module top-level strict code). Fix 1: use the method shorthand (name() {}). Fix 2: bind a regular function or restructure so the callback is an arrow nested inside a regular method, as in the timer example.
Exercise
What does () => { a: 1 } return, and why?
Show answer
undefined. The braces are parsed as a function body, a: 1 is a labeled statement, and nothing is returned. To return an object literal you need parentheses: () => ({ a: 1 }).
Spotted a mistake, or code that does not work?