Episode 1 — Fundamentals / 1.20 — Functions
1.20.d — Arrow Functions
In one sentence: Arrow functions (
=>) provide a shorter syntax for writing function expressions, lexically bindthis(they do not have their ownthis), lack theargumentsobject, and cannot be used as constructors — making them ideal for callbacks and array methods but unsuitable for object methods and prototype functions.
Navigation: ← 1.20.c — Return Values and Scope · 1.20.e — Writing Reusable Logic →
1. Arrow function syntax variations
Arrow functions were introduced in ES6 and come in several forms depending on parameter count and body complexity:
No parameters
const greet = () => "Hello!";
console.log(greet()); // "Hello!"
One parameter (parentheses optional)
const double = n => n * 2;
// Same as: const double = (n) => n * 2;
console.log(double(5)); // 10
Style note: Many teams require parentheses even for single parameters for consistency. ESLint's
arrow-parensrule enforces this.
Multiple parameters (parentheses required)
const add = (a, b) => a + b;
console.log(add(3, 4)); // 7
Block body (curly braces — explicit return needed)
const getFullName = (first, last) => {
const full = `${first} ${last}`;
return full.toUpperCase();
};
console.log(getFullName("Alice", "Smith")); // "ALICE SMITH"
With rest parameters
const sum = (...nums) => nums.reduce((total, n) => total + n, 0);
console.log(sum(1, 2, 3, 4)); // 10
With destructuring
const getName = ({ first, last }) => `${first} ${last}`;
console.log(getName({ first: "Bob", last: "Jones" })); // "Bob Jones"
2. Implicit return (no curly braces)
When the arrow function body is a single expression (no {}), the result is automatically returned:
// Implicit return — the expression IS the return value
const square = n => n * n;
// Equivalent with block body and explicit return
const squareVerbose = n => {
return n * n;
};
Multi-line implicit return with parentheses
You can wrap a long expression in () to keep the implicit return across lines:
const makeUser = (name, age) => ({
name,
age,
createdAt: Date.now(),
});
// Note: wrapping in () is REQUIRED for object literals (see Section 3)
3. Returning object literals
A common gotcha — curly braces are ambiguous between a block body and an object literal:
// BUG: JS interprets {} as a block, not an object
const getObj = () => { key: "value" };
console.log(getObj()); // undefined — the { key: "value" } is a labeled statement!
// FIX: Wrap the object in parentheses
const getObjFixed = () => ({ key: "value" });
console.log(getObjFixed()); // { key: "value" }
This is one of the most common arrow function mistakes. Always wrap object literal returns in parentheses.
4. this binding — the fundamental difference
This is the most important difference between arrow functions and regular functions.
Regular functions: this depends on HOW the function is called
const user = {
name: "Alice",
greet: function () {
console.log(`Hello, I'm ${this.name}`);
},
};
user.greet(); // "Hello, I'm Alice" — this = user (method call)
const fn = user.greet;
fn(); // "Hello, I'm undefined" — this = global/undefined (standalone call)
Arrow functions: this is inherited from the enclosing lexical scope
Arrow functions do not have their own this. They capture this from the surrounding scope at the time they are defined:
const user = {
name: "Alice",
greet: function () {
// Regular function — 'this' is the user object
const inner = () => {
// Arrow function — 'this' is inherited from greet's scope
console.log(`Hello, I'm ${this.name}`);
};
inner(); // "Hello, I'm Alice" — arrow inherits 'this' from greet
},
};
user.greet();
The classic setTimeout problem and solution
// PROBLEM with regular function
const timer = {
seconds: 0,
start: function () {
setInterval(function () {
this.seconds++; // 'this' is NOT timer — it's the global object
console.log(this.seconds); // NaN
}, 1000);
},
};
// SOLUTION 1: Arrow function (modern)
const timerFixed = {
seconds: 0,
start: function () {
setInterval(() => {
this.seconds++; // 'this' IS timer — arrow inherits from start()
console.log(this.seconds); // 1, 2, 3, ...
}, 1000);
},
};
// SOLUTION 2: const self = this (old pattern)
const timerOld = {
seconds: 0,
start: function () {
const self = this;
setInterval(function () {
self.seconds++;
console.log(self.seconds);
}, 1000);
},
};
5. No arguments object
Arrow functions do not have their own arguments object. If you reference arguments, it comes from the enclosing regular function (or throws a ReferenceError at the top level):
function outer() {
const inner = () => {
console.log(arguments); // Inherited from outer — [1, 2, 3]
};
inner();
}
outer(1, 2, 3);
// At top level — no enclosing function
const topLevel = () => {
// console.log(arguments); // ReferenceError in strict mode / modules
};
Solution: Use rest parameters instead:
const sum = (...args) => args.reduce((a, b) => a + b, 0);
console.log(sum(1, 2, 3)); // 6
6. Cannot be used as constructors
Arrow functions cannot be called with new:
const Person = (name) => {
this.name = name;
};
// const p = new Person("Alice"); // TypeError: Person is not a constructor
Arrow functions lack the internal [[Construct]] method and prototype property. Use regular functions or classes for constructors.
7. Cannot be used as object methods (this problem)
Because arrow functions inherit this from the enclosing scope (not the object), they make poor methods:
const user = {
name: "Alice",
// BAD — arrow function as method
greetArrow: () => {
console.log(`Hi, I'm ${this.name}`); // 'this' is global/module scope, NOT user
},
// GOOD — regular function or shorthand
greetRegular() {
console.log(`Hi, I'm ${this.name}`); // 'this' is user
},
};
user.greetArrow(); // "Hi, I'm undefined"
user.greetRegular(); // "Hi, I'm Alice"
Also problematic with prototypes
function Dog(name) {
this.name = name;
}
// BAD
Dog.prototype.bark = () => {
console.log(`${this.name} says woof`); // 'this' is not the Dog instance
};
// GOOD
Dog.prototype.bark = function () {
console.log(`${this.name} says woof`);
};
8. When to use arrow functions vs regular functions
| Scenario | Use | Why |
|---|---|---|
Callbacks (.map, .filter, .forEach) | Arrow | Concise; this inheritance usually helpful |
setTimeout / setInterval callbacks | Arrow | Inherits this from enclosing method |
| Event handlers (in frameworks) | Arrow (usually) | Inherits component's this in class-based React |
| Object methods | Regular (or shorthand) | Need this to refer to the object |
| Prototype methods | Regular | Need this to refer to the instance |
| Constructors | Regular / class | Arrow functions cannot be called with new |
Functions needing arguments | Regular | Arrow functions lack arguments |
| IIFE | Either | Arrow IIFEs work: (() => { ... })() |
| Generator functions | Regular | No arrow syntax for function* |
| Simple one-liners | Arrow | Implicit return is cleaner |
Decision flowchart (text)
Need 'new' (constructor)? → Regular function / class
Need own 'this' (method/prototype)? → Regular function / shorthand
Need 'arguments' object? → Regular function
Otherwise? → Arrow function (shorter, cleaner)
9. Real examples
Example 1: Array methods (arrows shine)
const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Carol", age: 35 },
];
// Filter, map, sort — all using arrow functions
const result = users
.filter(user => user.age >= 30)
.map(user => user.name)
.sort((a, b) => a.localeCompare(b));
console.log(result); // ["Alice", "Carol"]
Example 2: Event handler in a class component pattern
class ClickTracker {
constructor(element) {
this.count = 0;
this.element = element;
// Arrow function preserves 'this' as the ClickTracker instance
this.element.addEventListener("click", () => {
this.count++;
console.log(`Clicked ${this.count} times`);
});
}
}
Example 3: Chained promises
fetch("/api/user")
.then(response => response.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts))
.catch(err => console.error("Failed:", err));
Example 4: Composing small utilities
const pipe = (...fns) => (input) => fns.reduce((val, fn) => fn(val), input);
const trim = str => str.trim();
const lower = str => str.toLowerCase();
const slug = str => str.replace(/\s+/g, "-");
const makeSlug = pipe(trim, lower, slug);
console.log(makeSlug(" Hello World ")); // "hello-world"
Example 5: Returning objects from .map
const names = ["Alice", "Bob", "Carol"];
// Wrap object literal in parentheses for implicit return
const users = names.map((name, index) => ({
id: index + 1,
name,
active: true,
}));
console.log(users);
// [{ id: 1, name: "Alice", active: true }, ...]
Key takeaways
- Arrow functions use
=>and come in several forms: no-param(), single-param (optional parens), multi-param, and block body. - Implicit return works when there are no curly braces — the expression is the return value.
- Returning an object literal requires wrapping in
()— otherwise JS interprets{}as a block. - Arrow functions have no own
this— they inheritthisfrom the enclosing lexical scope. This is the biggest behavioral difference. - Arrow functions have no
argumentsobject — use rest parameters instead. - Arrow functions cannot be constructors (no
new) and should not be used as object methods or prototype functions. - Use arrows for callbacks, array methods, and short utilities. Use regular functions when you need
this,arguments,new, or generators.
Explain-It Challenge
Explain without notes:
- What does "lexical
this" mean in the context of arrow functions? - Why does using an arrow function as an object method cause
thisto be wrong? - Show the fix for
() => { key: "value" }and explain why it is needed. - In what scenario would an arrow function inside
setTimeoutbe better than a regular function?
Navigation: ← 1.20.c — Return Values and Scope · 1.20.e — Writing Reusable Logic →