2 min read
On this page

Scope, Closures & this

Scope

Scope determines where a variable is accessible. JavaScript has three levels.

Global, Function & Block Scope

const appName = "MyApp";  // global — accessible everywhere

function createUser() {
  const name = "Alice";   // function scope — only inside createUser
  if (true) {
    let x = 10;           // block scope — only inside this if
    var y = 20;           // function scope — leaks out of the block
  }
  // x is not accessible here. y is.
}

let and const are block-scoped. var is function-scoped and ignores blocks.

Lexical Scope

JavaScript uses lexical (static) scoping. A function accesses variables from where it is written in the source code, not where it is called.

const outer = "I am outer";
function parent() {
  const inner = "I am inner";
  function child() {
    console.log(outer);  // accessible — lexical parent chain
    console.log(inner);  // accessible — direct parent
  }
  child();
}

Closures

A closure is a function that remembers variables from its lexical scope, even after the outer function has returned.

function createCounter() {
  let count = 0;
  return function() {
    count += 1;
    return count;
  };
}
const counter = createCounter();
console.log(counter());  // 1
console.log(counter());  // 2

The returned function closes over count. Even though createCounter has finished, the inner function still has access.

Data Privacy

Closures create private state in JavaScript.

function createBankAccount(initialBalance) {
  let balance = initialBalance;
  return {
    deposit(amount) { balance += amount; return balance; },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() { return balance; }
  };
}
const account = createBankAccount(100);
account.deposit(50);       // 150
account.getBalance();      // 150
// account.balance is undefined — truly private

Factory Functions

function createLogger(prefix) {
  return {
    log(message) { console.log(`[${prefix}] ${message}`); },
    error(message) { console.error(`[${prefix}] ERROR: ${message}`); }
  };
}
const dbLogger = createLogger("DB");
dbLogger.log("Connected");  // [DB] Connected

Event Handlers

Closures let event handlers remember state without global variables.

function setupClickCounter(buttonId) {
  let clicks = 0;
  const button = document.querySelector(`#${buttonId}`);
  button.addEventListener("click", () => {
    clicks += 1;
    button.textContent = `Clicked ${clicks} times`;
  });
}

The this Keyword

this is determined by how a function is called, not where it is defined.

Default Binding

Standalone call: this is undefined in strict mode, globalThis otherwise.

"use strict";
function showThis() { console.log(this); }
showThis();  // undefined

Implicit Binding

Method call: this is the object before the dot.

const user = {
  name: "Alice",
  greet() { console.log(`Hello, I am ${this.name}`); }
};
user.greet();  // "Hello, I am Alice"

const greetFn = user.greet;
greetFn();     // this is lost — undefined in strict mode

Explicit Binding: call, apply & bind

function greet(greeting) {
  console.log(`${greeting}, I am ${this.name}`);
}
const alice = { name: "Alice" };

greet.call(alice, "Hello");      // call: args individually
greet.apply(alice, ["Hi"]);      // apply: args as array
const bound = greet.bind(alice); // bind: returns new function
bound("Hey");

bind is essential for callbacks: setTimeout(user.greet.bind(user), 100).

new Binding

new creates a fresh object and sets this to it.

function User(name) { this.name = name; }
const alice = new User("Alice");
console.log(alice.name);  // "Alice"

Arrow Functions & Lexical this

Arrow functions have no this of their own. They inherit from the enclosing scope.

const team = {
  name: "Engineering",
  members: ["Alice", "Bob"],
  listMembers() {
    this.members.forEach(member => {
      console.log(`${member} is on ${this.name}`);  // works — arrow inherits this
    });
  }
};

A regular function inside forEach would lose this.

Priority Order (highest first)

  1. new binding
  2. Explicit binding (bind, call, apply)
  3. Implicit binding (method call)
  4. Default binding (standalone call)
  5. Arrow functions always use lexical this (cannot be overridden)

Common Pitfalls

Losing this in callbacks. Passing a method as a callback detaches it from its object. Fix with .bind() or an arrow function wrapper.

Using arrow functions as object methods. Arrow functions inherit this from the enclosing scope, not the object.

const user = {
  name: "Alice",
  greet: () => console.log(`Hello, ${this.name}`)  // this is not user
};

Closures capture by reference, not value. The closure sees the variable's current value at run time.

let message = "hello";
const fn = () => console.log(message);
message = "goodbye";
fn();  // "goodbye"

The classic loop bug. var in a loop shares one variable across all iterations. Use let.

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
}

Key Takeaways

  • const and let are block-scoped. var is function-scoped.
  • Lexical scope means a function accesses variables from where it was written, not where it is called.
  • A closure is a function plus its lexical environment. Closures enable private state, factories, and stateful callbacks.
  • this is determined by the call site, not the definition. Four binding rules with a clear priority order.
  • Arrow functions inherit this from their enclosing scope, making them ideal for callbacks inside methods.
  • Use bind to preserve this when passing methods as callbacks.