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-
vartrap, 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
- Concise body:
x => x * 2returns the expression. With braces,x => { return x * 2; }, you needreturn. - To return an object literal, wrap it in parentheses:
() => ({ ok: true }). - Arrows have no own
this,arguments,superornew.target. They use the ones of the surrounding code (lexicalthis). - Arrows can't be used with
newand have noprototype.
❌ 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 form | this 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 function | whatever 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
undefinedand are evaluated per call....restis a real array, replacingarguments. - Arrows have lexical
thisand noarguments, and they can't be constructors. Wrap returned object literals in( ). thisis decided at call time:new>call/apply/bind> method call > plain call (undefinedin strict mode).- Closures capture variables.
letin 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.