Beyond the basics
The previous post covered how functions are defined and called. This one tackles the parts that come up in every interview and every code review: closures, the this binding, and the higher-order patterns that make JavaScript expressive.
Closures
A closure is the combination of a function and the lexical environment it was created in. When a function is created, it captures references to every variable it can see at that moment. Those variables remain alive for as long as the function does — even after the outer scope has otherwise been garbage-collected.
1function counter() {
2 let n = 0;
3 return {
4 inc: () => ++n,
5 get: () => n,
6 };
7}
8
9const c = counter();
10c.inc(); c.inc(); c.inc();
11c.get(); // 3
n is private — there is no way to reach it except through the returned functions. That single property is the basis of every module pattern, every memoiser, every event-debouncer, and the React hook system.
Closures over loop variables
This is the classic bug that drives every newcomer to switch from var to let:
1// With var — all three log 3
2for (var i = 0; i < 3; i++) {
3 setTimeout(() => console.log(i), 0);
4}
5
6// With let — logs 0, 1, 2
7for (let i = 0; i < 3; i++) {
8 setTimeout(() => console.log(i), 0);
9}
let creates a fresh binding for every iteration, so each callback closes over its own i. var declarations live for the whole enclosing function, so every callback shares one.
A practical closure: memoisation
1function memoise(fn) {
2 const cache = new Map();
3 return (arg) => {
4 if (!cache.has(arg)) cache.set(arg, fn(arg));
5 return cache.get(arg);
6 };
7}
8
9const slowSquare = (n) => { /* imagine work */ return n * n; };
10const fastSquare = memoise(slowSquare);
cache is invisible to the outside world but persists across calls.
The this binding
this is a function’s receiver — the value the function was called on. It is determined when the function is called, not when it is defined, and it is computed using four rules in priority order:
| Rule | Triggered by | Value of this |
|---|---|---|
new | new Foo() | The brand-new object. |
| Explicit | fn.call(x), fn.apply(x), fn.bind(x) | x. |
| Implicit | obj.method() | obj. |
| Default | Plain call fn() | undefined in strict mode; global object otherwise. |
1"use strict";
2const dog = {
3 name: "Rex",
4 bark() { return `${this.name} says woof`; },
5};
6
7dog.bark(); // "Rex says woof" ← implicit
8
9const b = dog.bark;
10b(); // TypeError: this is undefined ← default
11
12b.call({ name: "Spot" }); // "Spot says woof" ← explicit
Important
Detaching a method from its object loses the implicit binding. This is why every event handler that uses
thisis written either asobj.method.bind(obj)or as an arrow function.
Arrow functions and this
Arrow functions do not have their own this. They use whatever this was in scope when they were defined. That makes them perfect for callbacks inside methods:
1class Timer {
2 constructor() {
3 this.seconds = 0;
4 setInterval(() => {
5 this.seconds++; // ← `this` is the Timer instance
6 }, 1000);
7 }
8}
Replacing the arrow with a function expression would break this: setInterval calls its callback with this === undefined in strict mode.
call, apply, and bind
| Method | Calls now? | Passes arguments as |
|---|---|---|
fn.call(thisArg, a, b, c) | Yes | A comma-separated list. |
fn.apply(thisArg, [a, b, c]) | Yes | A single array. |
fn.bind(thisArg, a, b) | No | Returns a new function with this and any leading arguments pre-bound. |
1function greet(greeting, name) {
2 return `${greeting}, ${name}`;
3}
4
5greet.call(null, "Hi", "Ali"); // "Hi, Ali"
6greet.apply(null, ["Hi", "Ali"]); // "Hi, Ali"
7
8const hi = greet.bind(null, "Hi");
9hi("Ali"); // "Hi, Ali"
Modern alternatives have made apply largely obsolete: Math.max(...nums) replaces Math.max.apply(null, nums). bind remains essential for fixing this in callbacks.
Higher-order functions
A higher-order function either takes a function as an argument, returns a function, or both. JavaScript’s standard library is built around them:
1// Takes a function
2[1, 2, 3].map((x) => x * 2);
3
4// Returns a function
5function multiplyBy(factor) {
6 return (x) => x * factor;
7}
8const double = multiplyBy(2);
9
10// Both
11function pipe(...fns) {
12 return (input) => fns.reduce((acc, fn) => fn(acc), input);
13}
The most useful patterns:
Currying
Turning a multi-argument function into a chain of single-argument functions. Useful when the early arguments are “configuration” and the last argument is the data:
1const tag = (prefix) => (message) => `[${prefix}] ${message}`;
2
3const log = tag("LOG");
4const error = tag("ERR");
5
6log("ready"); // "[LOG] ready"
7error("boom"); // "[ERR] boom"
Partial application
Pre-supply some arguments now, accept the rest later. bind is the standard tool:
1const greet = (greeting, name) => `${greeting}, ${name}`;
2const hi = greet.bind(null, "Hi");
3hi("Ali"); // "Hi, Ali"
Composition
Combine small functions into a pipeline:
1const trim = (s) => s.trim();
2const lower = (s) => s.toLowerCase();
3const slug = (s) => s.replace(/\s+/g, "-");
4
5const toSlug = pipe(trim, lower, slug);
6toSlug(" Hello World "); // "hello-world"
A mental map of function patterns
Summary
- A closure captures the variables in scope at the time the function was created.
thisis determined by the call site, using four rules in priority order (new→ explicit → implicit → default).- Arrow functions inherit
thisfrom their enclosing scope — use them for callbacks, never for methods or constructors. call,apply, andbindsetthisexplicitly. Spread (...) replaces most uses ofapply.- Higher-order functions enable currying, partial application, and composition — the building blocks of functional programming.
Next: Modern function features — destructured parameters, async/await, generators, and tagged template literals.










