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:
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:
console.log(a); // undefined
var a = 5;
During the creation phase:
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:
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
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.