JavaScript Functions in Depth — Closures, this, and Higher-Order Patterns

A deep dive into the parts of JavaScript functions that everyone has to learn the hard way — closures, the `this` binding, `call` / `apply` / `bind`, higher-order functions, and the patterns built on top of them.

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:

RuleTriggered byValue of this
newnew Foo()The brand-new object.
Explicitfn.call(x), fn.apply(x), fn.bind(x)x.
Implicitobj.method()obj.
DefaultPlain 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

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

MethodCalls now?Passes arguments as
fn.call(thisArg, a, b, c)YesA comma-separated list.
fn.apply(thisArg, [a, b, c])YesA single array.
fn.bind(thisArg, a, b)NoReturns 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.
  • this is determined by the call site, using four rules in priority order (new → explicit → implicit → default).
  • Arrow functions inherit this from their enclosing scope — use them for callbacks, never for methods or constructors.
  • call, apply, and bind set this explicitly. Spread (...) replaces most uses of apply.
  • 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.

Attempted Questions: 0 / 8