Chapter 4 · Foundations Revisited

Functions & Closures

Functions are values in JavaScript: you pass them, return them and store them. This chapter covers the ES6 function features (arrows, default and rest parameters), the rules behind this, and closures, the idea that powers callbacks, factories and private state.

You will learn

  • The four ways to create a function, and how they differ.
  • Default parameters, rest parameters and spread arguments ES2015.
  • Arrow functions and lexical this.
  • The rules that decide this: plain call, method call, call/apply/bind, new.
  • Closures: what they are, the loop-var trap, and practical uses.

Four ways to create a function

Functions are objects: they have properties such as name and length (the number of declared parameters before the first default or rest parameter), and they can be passed to and returned from other functions. A function that takes or returns a function is a higher-order function.

Parameters ES2015

Default parameters

A default applies only when the argument is undefined (missing, or passed as undefined), not for null, 0 or "". Defaults are evaluated at call time, and they can use earlier parameters.

Rest parameters and the old arguments object

...rest collects the remaining arguments into a real array. It replaces the old arguments object, which is array-like but not an array, and doesn't exist in arrow functions.

📝 Older syntax you'll still see

✅ Rest parameter

Spread at the call site

The second line is the ES5 way. Spread (...) reads better and works with any iterable.

Arrow functions ES2015

❌ Braces mean a block, not an object

✅ Parentheses around the object

How this is decided

For normal functions, this is decided by how the function is called, not where it's defined. Check the rules in this order:

Call formthis is
new Fn()the newly created object
fn.call(obj), fn.apply(obj), or fn.bind(obj)()obj
obj.fn() (method call)obj, the thing before the dot
fn() (plain call)undefined in strict mode/modules; the global object in sloppy scripts
arrow functionwhatever this was where the arrow was written

The classic bug: losing this in a callback

❌ A regular callback gets its own this

✅ An arrow keeps the method's this

💡 Rule of thumb: use method shorthand for object and class methods (they need their own this) and arrows for callbacks inside them (they should share the method's this).

☕ Java: Java's this is fixed to the instance, always. JavaScript's this is a hidden parameter, filled in at call time. That's why passing obj.method around "forgets" obj, a mistake Java never lets you make.

Closures

A closure is a function together with the variables it can see from the scope where it was created. The inner function keeps those variables alive even after the outer function has returned.

Analogy: a function leaves home with a backpack. Inside are the variables from where it was born. It carries the backpack wherever it goes, and it can still read and change what's inside.

Each call to makeCounter creates a new count, so orders and invoices don't share state.

The loop-var trap

❌ var: one shared variable

✅ let: a fresh binding per iteration

Closures capture variables, not values. With var there is only one i, which is 3 by the time the functions run. With let, each iteration gets its own i. This was one of the main motivations for let.

📝 Older syntax you'll still see: the IIFE, an immediately invoked function expression, (function () { var private = 1; })();. It was used to create a private scope before block scope and modules existed. Today a { } block or a module does the job.

More examples

1. Memoizing an expensive function

2. Partial application with bind and with arrows

3. A once() wrapper

4. Debounce (the closure behind every search box)

5. Recursion with a default accumulator

Recap

  • Function declarations are hoisted. Expressions and arrows are values assigned like any other.
  • Defaults apply only for undefined and are evaluated per call. ...rest is a real array, replacing arguments.
  • Arrows have lexical this and no arguments, and they can't be constructors. Wrap returned object literals in ( ).
  • this is decided at call time: new > call/apply/bind > method call > plain call (undefined in strict mode).
  • Closures capture variables. let in loops gives one binding per iteration. Closures power private state, memoization and debouncing.

Exercises

Exercise 1 🟢 easy refactor it

Rewrite as an arrow function with a concise body: function square(n) { return n * n; }

Show solution

A concise body (without braces) returns its expression automatically.

Exercise 2 🟢 easy predict the output

Given const f = (a, b = a * 2) => a + b;, what are f(1), f(1, 0) and f(1, undefined)?

Show solution

3, 1, 3. The default kicks in only for undefined; 0 is a real value.

Exercise 3 🟢 easy fix the bug

This returns undefined. Fix it.

Show solution

After =>, a { starts a function body, not an object. Parentheses force it to be read as an expression.

Exercise 4 🟢 easy build it

Write average(...nums), which returns the mean, or 0 when called with no arguments.

Show solution

nums is a real array, so length and reduce just work.

Exercise 5 🟡 medium predict the output

What does this print (in a module)?

Show solution

Page & Spine none none. The method call sets this to shop. The arrow uses the module's this (undefined). The destructured regular() is a plain call, so this is undefined.

Exercise 6 🟡 medium fix the bug

Clicking "buy" (simulated by calling handler()) fails, because this.items is undefined. Fix it in two different ways.

Show solution

Extracting a method loses its object. Either fix this with bind, or keep the method call (cart.add(...)) inside a wrapper.

Exercise 7 🟡 medium build it · playground

Create playground/ids.js exporting makeIdGenerator(prefix), which returns a function producing ORD-0001, ORD-0002 and so on. Two generators must not share counters.

Show solution

Each call creates a new closure with its own n.

Exercise 8 🟡 medium predict the output

What does this print?

Show solution

2 then 2. A closure reads the current value of the variable it captured. Scope is lexical (decided where getX is written), so the x inside outer is irrelevant.

Exercise 9 🔴 hard explain why

Why does new (() => {})() throw TypeError: … is not a constructor, while new (function () {})() works?

Show solution

Arrow functions have no [[Construct]] behaviour and no prototype property, because they have no own this to initialise. Regular functions can be called with new; that's how "classes" worked before ES2015 (Appendix A).

Exercise 10 🔴 hard mini-project · playground

Create playground/rate-limit.js exporting rateLimit(fn, max). It returns a wrapper that calls fn at most max times, then returns "limit reached". Its .remaining() reports how many calls are left, and .reset() starts over. It must preserve this when used as a method.

Show solution

It uses a closure (used), rest parameters, apply to forward this, and properties on a function object. An arrow wrapper would break this here.

Official docs