Skip to content
Educora
Advanced22 min15 / 27

Scope and closures

Understand how JavaScript finds variables: lexical scope, closures, IIFE and the module pattern — plus `this`, `bind`, `call` and `apply`.

Check yourself
In this lesson you will learn
  • Explain lexical scope, the scope chain, block scope and hoisting
  • Use closures, an IIFE and the module pattern to keep private state
  • Predict what this is in a call and fix it with bind, call, apply or an arrow function

A website has a “Like” button that must remember how many times it was pressed. If you keep the counter in a global variable, any other code can break it by accident. So how can a function “remember” its own variable after it has finished? The answer is a closure — one of the most powerful ideas in JavaScript and one of the most common interview topics.

Lexical scope and the scope chain

Scope is the part of the code where a variable is visible. JavaScript uses lexical scope: which variables a function can see depends on where it is written, not where it is called. Every function and every { } block with let/const creates a new scope. When looking up a variable, JavaScript checks the current scope, then the outer one, all the way to the global scope — this is the scope chain. The search only goes from the inside out, never the other way.

JavaScript
const city = 'Baku';

function outer() {
  const district = 'Nasimi';

  function inner() {
    const street = 'Nizami';
    console.log(street, district, city);
  }

  inner();
  console.log(typeof street);
}

outer();

if (true) {
  let blockVar = 'inside';
  var functionVar = 'var ignores blocks';
}
console.log(typeof blockVar, functionVar);
▸ Expected output
Nizami Nasimi Baku
undefined
undefined var ignores blocks
inner sees all three scopes, while outer cannot see street. var “escapes” the block, while let stays inside it.

Before running the code, the engine registers all declarations — this is called hoisting. A variable declared with var is “lifted” to the top of its scope with the value undefined. A function declaration is hoisted completely, so you can call it before the line where it is written. let and const are hoisted too, but until their line they stay in the temporal dead zone (TDZ): touching them early throws a ReferenceError.

JavaScript
console.log(early);
var early = 'var is hoisted';

try {
  console.log(later);
} catch (error) {
  console.log(error.name);
}
let later = 'let waits in the TDZ';

console.log(sayHi());

function sayHi() {
  return 'Function declarations are hoisted';
}
▸ Expected output
undefined
ReferenceError
Function declarations are hoisted

Closures

Definition
Closure

A function together with the variables of the scope where it was created. The inner function keeps access to those variables even after the outer function has finished.

Normally a function's local variables disappear when it ends. But if an inner function that uses them has been returned or stored somewhere, the engine keeps them alive. Each call of the outer function creates a new, separate set of variables — below, likes and views never get in each other's way.

JavaScript
function createCounter(start = 0) {
  let count = start;
  return {
    increment: () => ++count,
    decrement: () => --count,
    value: () => count,
  };
}

const likes = createCounter();
const views = createCounter(100);

likes.increment();
likes.increment();
views.increment();

console.log(likes.value(), views.value());
console.log(likes.count);
▸ Expected output
2 101
undefined
count is not a property of the object — it lives only in the closure, so nobody can change it from outside.

Another common use of closures is function factories: you create new functions that “remember” a parameter.

JavaScript
function makeMultiplier(factor) {
  return (x) => x * factor;
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

console.log(double(5), triple(5));
console.log([1, 2, 3].map(triple));
▸ Expected output
10 15
[ 3, 6, 9 ]
JavaScript
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log('var', i), 0);
}

for (let j = 1; j <= 3; j++) {
  setTimeout(() => console.log('let', j), 0);
}
▸ Expected output
var 4
var 4
var 4
let 1
let 2
let 3

IIFE and the module pattern

Before ES modules, developers hid variables with an IIFE — an Immediately Invoked Function Expression: a function wrapped in parentheses and called right away. Its variables do not “leak” into the global scope. Combine an IIFE with a closure and you get the module pattern: private data plus a public object (API) with only the methods you choose. Today ES modules do this job at the file level, but the pattern is still common in older code and useful inside a single file.

JavaScript
const bank = (function () {
  let balance = 0;
  const history = [];

  return {
    deposit(amount) {
      balance += amount;
      history.push(`+${amount}`);
    },
    withdraw(amount) {
      if (amount > balance) return 'Not enough money';
      balance -= amount;
      history.push(`-${amount}`);
      return 'OK';
    },
    report: () => ({ balance, history: [...history] }),
  };
})();

bank.deposit(100);
console.log(bank.withdraw(30), bank.withdraw(500));
console.log(bank.report());
console.log(typeof balance);
▸ Expected output
OK Not enough money
{ balance: 70, history: [ '+100', '-30' ] }
undefined
balance and history change only through deposit, withdraw and report. report returns a copy of the array so that nobody can damage the history from outside.

this, bind, call and apply

Unlike ordinary variables, this is not lexical: in a regular function its value depends on how the function is called and is decided at call time. Arrow functions have no this of their own — they take it from the surrounding code.

CallWhat this is
obj.method()the object left of the dot — obj
fn()undefined in strict mode (classes, modules), the global object in old sloppy code
new Fn()the newly created object
fn.call(obj, a, b), fn.apply(obj, [a, b])obj — the function is called at once
fn.bind(obj)a new function whose this is always obj
arrow functionthe this of the surrounding code
JavaScript
function introduce(greeting, punctuation) {
  return `${greeting}, I am ${this.name}${punctuation}`;
}

const aysel = { name: 'Aysel' };
const murad = { name: 'Murad' };

console.log(introduce.call(aysel, 'Hi', '!'));
console.log(introduce.apply(murad, ['Hello', '.']));

const ayselIntro = introduce.bind(aysel, 'Hey');
console.log(ayselIntro('!!'));

console.log(Math.max.apply(null, [3, 9, 4]));
▸ Expected output
Hi, I am Aysel!
Hello, I am Murad.
Hey, I am Aysel!!
9
call takes the arguments one by one, apply takes them as an array. bind does not call the function — it returns a new one with this (and, if you like, the first arguments) fixed.

When you pass a method as a callback, it gets “detached” from its object. Class methods always run in strict mode, so this becomes undefined and reading a property throws a TypeError. Fixes: bind, an arrow-function wrapper, or an arrow function written as a class field.

JavaScript
class Player {
  score = 0;

  addPoint() {
    this.score++;
  }

  addPointArrow = () => {
    this.score++;
  };
}

const player = new Player();
const detached = player.addPoint;

try {
  detached();
} catch (error) {
  console.log('Lost this:', error.name);
}

[1, 2].forEach(player.addPoint.bind(player));
[1, 2, 3].forEach(player.addPointArrow);
console.log(player.score);
▸ Expected output
Lost this: TypeError
5
Exercise

Write createIdGenerator(prefix): it returns a function that gives the next id on every call: user-1, user-2… Keep the counter in a closure, and give every generator its own counter.

Exercise · JavaScript
function createIdGenerator(prefix) {
  // keep a counter here
  return () => {
    // increase the counter and return `${prefix}-${count}`
  };
}

const userId = createIdGenerator('user');
const orderId = createIdGenerator('order');

console.log(userId());
console.log(userId());
console.log(orderId());
console.log(userId());
▸ Expected output
user-1
user-2
order-1
user-3
Exercise

finalPrice takes the discount from this.discount. 1) Use call to print the price of a 200-manat item for the shop object. 2) Use bind to create a function bound to shop, and pass it to map to convert the prices [50, 80].

Exercise · JavaScript
const shop = { discount: 10 };

function finalPrice(price) {
  return price - (price * this.discount) / 100;
}

// 1) finalPrice for 200 with call
// 2) bind to shop and use it in map for [50, 80]
▸ Expected output
180
[ 45, 72 ]

Key points

  • Lexical scope: a function sees the variables of the place where it is written; the lookup goes outward along the scope chain.
  • let/const are block-scoped and sit in the TDZ until their line; var is function-scoped and hoisted as undefined.
  • A closure is a function plus the variables it remembers; each call of the outer function creates a new set.
  • An IIFE and the module pattern hide private data and return only a public API.
  • this depends on how a function is called: call/apply set it once, bind fixes it for good, and arrow functions take it from outside.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
Given function outer() { let n = 0; return () => ++n; }, what does b() return after const a = outer(); const b = outer(); a(); a();?