JavaScript · 10 topics

Scope, closures and this

The ten topics that open more interview rounds than anything else in JavaScript, including the loop and setTimeout puzzle and writing your own bind.

Published
Reading time
22 min read
Topics covered
10 topics

Ten topics. Between them they produce more interview questions than anything else in JavaScript. Closures alone open perhaps a third of all screening rounds.

The good news: they are all the same idea seen from different angles. A function remembers where it was written. Get that, and the rest follows.

Lexical scope and the scope chain

In simple words: where a variable is available is decided by where you wrote the code, not by where you call it from. JavaScript decides this when it reads your file, before anything runs.

When a function needs a variable, it looks in its own body first. Not there? It looks at the function that wrapped it. Still not there? It keeps going outward until it reaches the global scope. That chain of lookups is the scope chain.

GLOBALconst app = "portfolio"function outer()const user = "Subham"function inner()reads user, then applookupgoes out
Lookup only ever travels outward. An inner function can read everything around it; nothing outside can read in.
const app = "portfolio";
 
function outer() {
  const user = "Subham";
 
  function inner() {
    console.log(user);  // not local -> found in outer
    console.log(app);   // not in outer -> found in global
  }
 
  inner();
}

Closures

In simple words: a closure is a function that still remembers the variables around it, even after the function that created those variables has finished.

That is the entire definition. The confusion comes from the fact that it sounds like it should be impossible.

function makeCounter() {
  let count = 0;                 // outer finishes, but this survives
 
  return function () {
    count += 1;
    return count;
  };
}
 
const next = makeCounter();
next();  // 1
next();  // 2
next();  // 3
 
const other = makeCounter();
other(); // 1  -- a completely separate count

makeCounter returned and its call finished. Normally count would be discarded. But the returned function still refers to it, so JavaScript keeps it alive.

AFTER makeCounter() HAS RETURNEDmakeCounter framediscardedcount = 3kept alivenext()the returned fnBecause next still points at count, the garbage collector cannot free it.Drop every reference to next and both the function and count are collected.
The outer call frame is gone, but the variable it created is kept alive because the returned function still refers to it.

The loop and setTimeout puzzle

In simple words: the classic trick question. It is really a var versus let question wearing a costume.

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3 3 3
 
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 0 1 2

Why. var is function scoped, so there is exactly one i for the whole loop. All three callbacks close over that same variable. By the time the timers fire, the loop has finished and i is 3.

let is block scoped, and the for loop creates a fresh binding on every iteration. Three separate variables, three separate closures, three different values.

Three ways to fix the var version

// 1. Use let. Always the right answer in real code.
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
 
// 2. IIFE - create a new scope by hand, the pre-ES6 fix
for (var i = 0; i < 3; i++) {
  (function (captured) {
    setTimeout(() => console.log(captured), 0);
  })(i);
}
 
// 3. Pass the value through setTimeout's extra arguments
for (var i = 0; i < 3; i++) {
  setTimeout((captured) => console.log(captured), 0, i);
}

this

In simple words: in a normal function, this is decided by how the function is called, not where it was written. Four rules, checked in order.

#RuleHow it looksWhat this is
1new bindingnew Person()the brand new object
2Explicit bindingfn.call(obj), fn.apply(obj), fn.bind(obj)whatever you passed
3Implicit bindingobj.method()the object left of the dot
4Default bindingfn()undefined in strict mode, globalThis otherwise

Arrow functions sit outside this list entirely. An arrow function has no this of its own. It uses the this from wherever it was written, and nothing can change that — not call, not bind.

const user = {
  name: "Subham",
  greet() {
    console.log(this.name);
  },
};
 
user.greet();                 // "Subham"  -- rule 3, object left of the dot
 
const detached = user.greet;
detached();                   // undefined -- rule 4, the dot is gone
 
setTimeout(user.greet, 0);    // undefined -- same problem, passed as a plain fn
setTimeout(() => user.greet(), 0);  // "Subham" -- the dot survives

call, apply and bind

In simple words: three ways to say "run this function, but with this set to something I choose".

  • call(thisArg, a, b) — runs immediately, arguments listed
  • apply(thisArg, [a, b]) — runs immediately, arguments in an array
  • bind(thisArg, a) — does not run, returns a new function with this locked in
function introduce(greeting, punctuation) {
  return `${greeting}, I am ${this.name}${punctuation}`;
}
 
const me = { name: "Subham" };
 
introduce.call(me, "Hello", "!");     // "Hello, I am Subham!"
introduce.apply(me, ["Hi", "."]);     // "Hi, I am Subham."
 
const boundIntro = introduce.bind(me, "Hey");
boundIntro("!!");                     // "Hey, I am Subham!!"

Implement your own bind

A very common senior-round request. Three things must work: this binding, partial application, and behaving correctly when the bound function is called with new.

Function.prototype.myBind = function (thisArg, ...boundArgs) {
  const originalFn = this;
 
  if (typeof originalFn !== "function") {
    throw new TypeError("Bind must be called on a function");
  }
 
  function bound(...callArgs) {
    // when used with `new`, ignore thisArg and use the fresh object
    const isNew = this instanceof bound;
    return originalFn.apply(isNew ? this : thisArg, [...boundArgs, ...callArgs]);
  }
 
  bound.prototype = Object.create(originalFn.prototype || null);
  return bound;
};

Arrow functions vs regular functions

In simple words: arrows are not just shorter syntax. They are missing several things on purpose.

Regular functionArrow function
thisset at call time by the four rulesinherited from where it was written
arguments objectyesno, use rest parameters
Usable with newyesno, throws TypeError
Has a prototype propertyyesno
Can be a generatoryesno
Hoistingdeclarations hoist fullyonly the variable hoists

When you must not use an arrow:

// 1. As an object method that needs `this`
const counter = {
  count: 0,
  bad: () => { this.count++; },      // `this` is not counter
  good() { this.count++; },
};
 
// 2. As a constructor
const Person = (name) => { this.name = name; };
new Person("Subham");                 // TypeError: not a constructor
 
// 3. As a prototype method
Dog.prototype.speak = () => this.name; // `this` is not the instance

When an arrow is exactly right: any callback inside a method, where you want the surrounding this.

class Timer {
  constructor() { this.seconds = 0; }
  start() {
    setInterval(() => { this.seconds++; }, 1000);  // arrow keeps `this`
  }
}

Closures you actually write

In simple words: closures are not a puzzle-only feature. Almost every utility function you use is one.

Private state

function createAccount(initial) {
  let balance = initial;                  // genuinely unreachable from outside
 
  return {
    deposit(amount) { balance += amount; return balance; },
    getBalance() { return balance; },
  };
}
 
const account = createAccount(100);
account.deposit(50);   // 150
account.balance;       // undefined -- there is no way in

once

function once(fn) {
  let called = false;
  let result;
 
  return function (...args) {
    if (called) return result;
    called = true;
    result = fn.apply(this, args);
    return result;
  };
}
 
const init = once(() => console.log("setting up"));
init();  // logs
init();  // silent

memoize

function memoize(fn, keyFrom = (...args) => JSON.stringify(args)) {
  const cache = new Map();
 
  return function (...args) {
    const key = keyFrom(...args);
    if (cache.has(key)) return cache.get(key);
 
    const value = fn.apply(this, args);
    cache.set(key, value);
    return value;
  };
}

Currying and partial application

In simple words: currying turns a function that takes three arguments into a chain of functions that each take one. Partial application just pre-fills some arguments and waits for the rest.

// plain
const add = (a, b, c) => a + b + c;
add(1, 2, 3);          // 6
 
// curried
const curriedAdd = (a) => (b) => (c) => a + b + c;
curriedAdd(1)(2)(3);   // 6

A generic curry

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...rest) => curried.apply(this, [...args, ...rest]);
  };
}
 
const sum = curry((a, b, c) => a + b + c);
sum(1)(2)(3);      // 6
sum(1, 2)(3);      // 6
sum(1)(2, 3);      // 6

fn.length is the number of declared parameters. The moment enough have arrived, it runs.

Infinite currying

A separate puzzle. sum(1)(2)(3)() — the empty call signals the end.

function sum(a) {
  let total = a;
  function next(b) {
    if (b === undefined) return total;
    total += b;
    return next;
  }
  return next;
}
 
sum(1)(2)(3)();   // 6

IIFE and the module pattern

In simple words: an IIFE is a function you define and call in the same breath. Before ES modules existed, it was the only way to get a private scope.

(function () {
  // everything here is invisible to the outside
})();
 
const CounterModule = (function () {
  let count = 0;                              // private
  return {
    increment() { return ++count; },
    reset() { count = 0; },
  };
})();

You rarely write these now — every ES module already has its own scope. But you will see them constantly in older code and in bundled output, so recognise the shape.

Composition and pipe

In simple words: small functions combined into bigger ones. pipe reads left to right, compose reads right to left.

const pipe = (...fns) => (input) => fns.reduce((acc, fn) => fn(acc), input);
const compose = (...fns) => (input) => fns.reduceRight((acc, fn) => fn(acc), input);
 
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const slugify = (s) => s.replace(/\s+/g, "-");
 
const toSlug = pipe(trim, lower, slugify);
toSlug("  Hello World  ");   // "hello-world"

Ten questions

Answer each out loud before revealing.

  1. 01
    Explain a closure to someone who has never heard the word, in two sentences.
  2. 02
    Why does a var loop with setTimeout print 3 3 3, and what are three ways to fix it?
  3. 03
    What is this inside a method that has been passed as a callback, and how do you fix it?
  4. 04
    Name the four rules for this in priority order, and say where arrow functions fit.
  5. 05
    Name four concrete differences between an arrow function and a regular function.
  6. 06
    Why does this fail to remove the listener? el.removeEventListener("click", this.handler.bind(this))
  7. 07
    What is the memory cost of a closure, and when does it become a real leak?
  8. 08
    How does a generic curry know when to stop collecting arguments and actually run?
  9. 09
    How would you give an object genuinely private state, without # fields?
  10. 10
    What does once(fn) need to remember, and why must it store the result rather than just a flag?

Six exercises

  1. 01

    Implement Function.prototype.myBind

    Support this binding and partial application. Then make it behave correctly when the bound function is called with new — the fresh object should win over the bound this. Test both paths.

  2. 02

    Write memoize with a size cap

    Start with the plain version, then add a maximum entry count that evicts the least recently used key. You have just built a small LRU cache, which is a separate interview question in its own right.

  3. 03

    Write a generic curry

    It should accept arguments one at a time, several at a time, or all at once. Then explain in one sentence why it cannot work on a function that uses rest parameters.

  4. 04

    Fix the loop three ways

    Take the var plus setTimeout loop and fix it with let, with an IIFE, and with setTimeout's extra arguments. Then replace the timers with an array of functions called later, and confirm you get the same results.

  5. 05

    Build a rate-limited function

    Write limitCalls(fn, n) that lets a function run at most n times, then returns the last result forever after. Reuse what you learnt from once.

  6. 06

    Write pipe and compose

    Implement both, then use pipe to build a slug generator from four small string functions. Explain which reads more naturally to you and why.

Common questions

What is a closure in JavaScript?
A closure is a function bundled with the scope it was created in. When an inner function references a variable from an outer function, that variable survives after the outer function returns, because the inner function still holds a reference to it. Each call to the outer function creates a fresh, independent set of those variables.
Why does a for loop with var and setTimeout print 3 3 3?
var is function scoped, so the whole loop shares one variable. All three callbacks close over that same variable, and by the time the timers fire the loop has finished with the value 3. Using let creates a fresh binding on every iteration, giving 0 1 2. You can also fix it with an IIFE or by passing the value as a third argument to setTimeout.
How is this determined in JavaScript?
In a normal function it is decided at call time by four rules in priority order: new binding, explicit binding with call apply or bind, implicit binding from the object before the dot, and the default binding which is undefined in strict mode. Arrow functions do not follow these rules at all - they take this lexically from where they were written.
What is the difference between call, apply and bind?
call and apply both invoke the function immediately with a chosen this, the only difference being that call takes arguments individually and apply takes them as an array. bind does not invoke anything - it returns a new function with this permanently fixed, optionally with some arguments pre-filled.
When should you not use an arrow function?
Do not use an arrow function as an object method or a prototype method that needs this, because it will capture the surrounding scope instead of the instance. Do not use one as a constructor, since arrows cannot be called with new. Arrows are the right choice for callbacks inside a method, where you want to keep the enclosing this.