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 bind this (they do not have their own this), lack the arguments object, 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-parens rule 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

ScenarioUseWhy
Callbacks (.map, .filter, .forEach)ArrowConcise; this inheritance usually helpful
setTimeout / setInterval callbacksArrowInherits this from enclosing method
Event handlers (in frameworks)Arrow (usually)Inherits component's this in class-based React
Object methodsRegular (or shorthand)Need this to refer to the object
Prototype methodsRegularNeed this to refer to the instance
ConstructorsRegular / classArrow functions cannot be called with new
Functions needing argumentsRegularArrow functions lack arguments
IIFEEitherArrow IIFEs work: (() => { ... })()
Generator functionsRegularNo arrow syntax for function*
Simple one-linersArrowImplicit 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

  1. Arrow functions use => and come in several forms: no-param (), single-param (optional parens), multi-param, and block body.
  2. Implicit return works when there are no curly braces — the expression is the return value.
  3. Returning an object literal requires wrapping in () — otherwise JS interprets {} as a block.
  4. Arrow functions have no own this — they inherit this from the enclosing lexical scope. This is the biggest behavioral difference.
  5. Arrow functions have no arguments object — use rest parameters instead.
  6. Arrow functions cannot be constructors (no new) and should not be used as object methods or prototype functions.
  7. 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:

  1. What does "lexical this" mean in the context of arrow functions?
  2. Why does using an arrow function as an object method cause this to be wrong?
  3. Show the fix for () => { key: "value" } and explain why it is needed.
  4. In what scenario would an arrow function inside setTimeout be better than a regular function?

Navigation: ← 1.20.c — Return Values and Scope · 1.20.e — Writing Reusable Logic →