Bindings in JavaScript​: the hidden glue behind this, closures, and scope
Every JavaScript developer on interview day

Bindings in JavaScript: the hidden glue behind this, closures, and scope

We all know the words: scope, hoisting, closures, this — but few people can clearly explain what’s really going on under the hood. Let’s fix that. 👇


What Is a Binding?

A binding is the connection between an identifier (a variable name) and a storage slot that holds its value. So, when you write: let x = 10, JavaScript doesn’t just “store 10”. It creates an internal record that says: “The name x points to this memory location.”

These records live inside special structures called Lexical Environments, and they’re the real backbone of JavaScript’s scoping system.

Lexical Environments — How Scopes Actually Work

Each function, block, and module creates its own Lexical Environment, and every environment has two parts:

  1. Environment Record — the table of bindings in that scope.
  2. Outer Reference — a pointer to the parent environment.

When JS engine looks up a variable, it climbs this chain: current scope → outer → global

function outer() {
  const a = 10;
  function inner() {
    console.log(a); // found in outer’s environment
  }
  inner();
}
outer();        

When inner is created, it keeps a hidden reference ([[Environment]]) to where it was defined. That’s how scope lookup works — no magic, just a chain of environments.


How closures work

A closure is a function that remembers variables from its outer scope, even after that scope has finished executing.

When a function is declared, it doesn’t copy variables — it captures the environment where it was created.

function makeCounter() {
  let count = 0;
  return function () {
    return ++count;
  };
}

const c1 = makeCounter();
const c2 = makeCounter();

console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 1        

Each makeCounter() call creates a new Lexical Environment, and each returned function holds a reference to its own count binding. That’s why c1 and c2 are independent — their closures point to different environment objects.


Hoisting — What It ActuallyMeans

Hoisting doesn’t mean “moving code to the top.” It means creating all bindings before execution starts. When your script runs, JS goes through two phases:

  1. Creation phase — sets up scopes and creates bindings.
  2. Execution phase — runs code line by line.

console.log(a); // undefined
var a = 5;        

During the creation phase:

  • var a binding is created and initialized as undefined.

For let and const, bindings exist but remain uninitialized until their declaration is reached → the Temporal Dead Zone (TDZ):

console.log(b); // ❌ ReferenceError
let b = 5;        

The this Binding — Dynamic, Not Lexical

function greet() {
  console.log(this.name);
}

const user = { name: 'Alice', greet };
user.greet(); // Alice ✅

const fn = user.greet;
fn(); // undefined ❌ — lost the binding        

Here, the second call loses the user context — because the call isn’t obj.method() anymore.


Explicit Binding — .call(), .apply(), .bind()

.call(thisArg, ...args)

Calls the function immediately with a specific this:

greet.call(user); // Alice        

.apply(thisArg, argsArray)

Same as .call(), but arguments are passed as an array:

greet.apply(user, []);        

.bind(thisArg, ...presetArgs)

Creates a new function with a permanently fixed this:

const sayHi = user.greet.bind(user);
sayHi(); // Alice        

Binding Priority — Who Wins?

When multiple binding rules apply, JavaScript resolves them in this order:

Article content
binging priority table
function Person(name) { this.name = name; }
const Bound = Person.bind({ name: 'X' });
const p = new Bound('Alice');
console.log(p.name); // "Alice" — `new` beats `bind`        

One-Time Binding Rule

Once a function is bound, it stays bound — forever.

function show() { console.log(this.x); }
const f1 = show.bind({ x: 1 });
const f2 = f1.bind({ x: 2 });
f2(); // 1 — the first binding “sticks”        

.bind() wraps the function and stores [[BoundThis]]; calling .bind() again won’t override it.


Arrow Functions and this

Arrow functions (=>) behave very differently from regular functions: they don’t create their own this, arguments, super, or new.target.

const user = {
  name: 'Alice',
  sayHi: () => console.log(this.name)
};
user.sayHi(); // ❌ undefined        

They ignore .call(), .apply(), and .bind() — because their this is fixed at creation time.

Key Takeaways

  • A binding links a name to a memory slot, not a copied value.
  • Every scope = a Lexical Environment with an outer link.
  • Hoisting = binding creation before execution.
  • this is bound dynamically at call time.
  • Priority: new → bind/call/apply → implicit → default.
  • Bound functions stay bound — forever.
  • Arrow functions capture this lexically.


Once you truly understand bindings — how names, scopes, and this all connect JavaScript stops feeling magical. It becomes a simple chain of environments and rules. And that’s when you really start writing clean, predictable code.

To view or add a comment, sign in

More articles by Anton Kovalev

Explore content categories