Language Internals
Part 3 of 6 · JS/TS Language ProficiencyClosures, Scope, this Binding & Prototypes
Lexical scope, closures, this binding rules, prototypes vs class sugar.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does lexical scope mean?
Answer
A binding is visible where it is written, to nested functions, not based on who calls them.
L2
Why does a var loop log 3, 3, 3 from three timers?
Answer
var is one function-scoped binding. Each callback reads it after the loop has finished.
L3
What is the temporal dead zone?
Answer
let and const exist in the block but throw if you touch them before the initializing line.
L4
What sets this for a normal function?
Answer
new, then call/apply/bind, then the receiver of a method call, then undefined in strict mode or the global object in sloppy mode.
L5
Why does an arrow method fail to see the object?
Answer
Arrows have no this of their own. They use the enclosing this, which is often undefined at the top level.
L6
What is the difference between Func.prototype and obj.__proto__?
Answer
Func.prototype is the object new Func assigns as [[Prototype]]. __proto__ or Object.getPrototypeOf reads an object's [[Prototype]].
L7
How do private fields differ from a closure?
Answer
A #field is per instance and brand-checked by the engine. A closure hides state in the environment and does not put it on the instance.
Failure modes
Shared var binding
Every timer or callback closes over the same i, so they all observe the final value.
Detached method
Passing obj.method without its receiver makes this undefined in strict mode.
Arrow used as a method
The arrow captures the outer this and never becomes the object it was stored on.
Prototype versus own property mix-up
Someone assigns on the instance and thinks they replaced the shared method, or the reverse.
Closure retains a large scope
A small callback closes over an object it only needed one field from, so the whole object stays reachable.
Misconceptions
A closure copies the values it uses.
It captures the binding. Later assignments to that binding are visible, which is the var-loop bug.
this is the function's owner.
this is set by how the function is called, unless it is an arrow or it was bound.
class creates a new object model.
class writes a constructor and puts methods on Constructor.prototype. extends links the prototype chain.
Interviewer traps
Saying arrows fix methods.
Arrows freeze the outer this. Methods that need the receiver should be concise methods or an explicit bind.
Using __proto__ and prototype as synonyms.
prototype is the object a constructor installs. The internal prototype is the link on an instance. getPrototypeOf reads the link.
Putting private data on this and calling it encapsulated.
Own properties are visible. A closure or a private field is what actually hides the state.
Design scenario
Same prompt for every reader.
Requirements
Name the binding rules you would apply, which form you would keep, and what the closure is retaining.
Traffic / scale
Thousands of short callbacks per second on one realm.
Latency
The bug is correctness of this and of the loop binding, not a queue delay.
Consistency
Each callback must see the receiver and the iteration it was created for.
Availability
The process should throw a clear TypeError rather than write through a global this.
Failure assumptions
- The method is extracted and passed as a bare function.
- The loop used var.
- Strict mode is on.
Constraints
- Answer with scope, this, and the prototype chain.
- A framework hook is not the explanation.
Prompt
A callback registered as obj.save later throws because this is undefined, and a loop of timers prints the same index three times.
Where should behavior live?
Prefer
Shared prototype method
One function on Constructor.prototype, with this set by the call. Instances stay small. class methods are this shape.
- The method is shared, not copied per object.
- obj.method() sets the receiver.
- A subclass overrides by putting a function earlier in the chain.
Alternative
Arrow or closure per instance
Useful for private state and for callbacks that must keep an outer this. Expensive and surprising when used as the method itself.
- Each object carries its own function.
- The arrow will not pick up a later receiver.
- The closed-over environment stays reachable with the function.
Resolve this
Stop at the first rule that applies. Arrows never enter the list.
- 1
new
The constructor's this is the fresh object. - 2
call, apply, or bind
The explicit receiver wins, including a bound function's stored receiver. - 3
Method call
obj.fn() sets this to obj. - 4
Bare call
Strict mode uses undefined. Sloppy mode uses the global object.
Overview
Lexical scope means the text of the program decides which binding a name sees. A closure is a function plus the environment of the free variables it references. That is how modules, partial application, and hooks keep state. It is also how a callback accidentally retains a huge object. The memory page owns the leak. This page owns the capture.
this is not a scope variable for ordinary functions. It is filled in at the call. Arrow functions are the exception: they close over the enclosing this and do not get one of their own.
Prototypes are the delegation chain. class is sugar that writes that chain for you.
Scope and closures
Flow
- 1
1. Global environment
- next2. Outer environment x
- 2
2. Outer environment x
- next3. Inner closes over x
- 3
3. Inner closes over x
Lesson map
Closures, Scope, this Binding & Prototypes
Lexical scope, closures, this binding rules, prototypes vs class sugar.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB global["1. Global environment"] outer["2. Outer environment x"] inner["3. Inner closes over x"] global -->|1. Global environment| outer outer -->|2. Outer environment x| inner
The inner function keeps the outer environment alive for as long as the inner function itself is reachable. Capturing x does not copy x. Later writes to that binding are visible.
var, let, const, and the dead zone
| Binding | Hoisting | Redeclare in the same scope | Scope | Temporal dead zone |
|---|---|---|---|---|
var | Binding is created and set to undefined | Yes, in the same function | Function | No |
let | Binding is created | No | Block | Yes, until the initializer |
const | Binding is created | No | Block | Yes, and no reassignment |
Classic trap: for (var i = 0; i < 3; i++) plus a timer logs 3, 3, 3 because every callback sees the one var binding after the loop. let gives each iteration its own binding. The timers on the event loop page are why you observe the value later. The bug is the binding, not the queue.
Press Run. Snippets must be self-contained — no network, files, or native modules.
this
const obj = {
n: 1,
method() {
return this.n;
},
arrow: () => this,
};
const detached = obj.method;
obj.method(); // 1
// detached(); // TypeError in strict mode. The receiver was left at the door.Press Run. Snippets must be self-contained — no network, files, or native modules.
Priority, first match wins: new, then call / apply / bind, then a method call, then a bare call. Arrows skip the list.
Prototypes and class syntax
Flow
- 1
1. Instance
- next2. Constructor.prototype
- 2
2. Constructor.prototype
- next3. Object.prototype
- 3
3. Object.prototype
new Func sets the instance's internal prototype to Func.prototype. Func.prototype is not the same slot as obj.__proto__ or Object.getPrototypeOf(obj). The second reads the link. The first is what a constructor installs.
class is a constructor plus methods on the prototype. Instance fields are own properties, created per object. Methods are shared. extends links the prototype chain, and super calls the parent constructor or method. Own versus inherited is Object.hasOwn.
Private fields (#x) are per instance and brand-checked. A closure hides state by never putting it on the object. Both are privacy. They are not the same mechanism.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Interview Q&A
Why do arrows fail when you use them as object methods?
Answer
An arrow does not receive this from the call site. It closes over the outer this, often undefined in strict mode or at module scope. Use a concise method, or bind, when the function must see the object it was stored on.
What is the difference between obj.__proto__ and Func.prototype?
Answer
Func.prototype is the object new Func installs as the instance's internal prototype. __proto__, or Object.getPrototypeOf, reads that link on an existing object. Assigning one is not the same operation as reading the other.
How do private fields differ from closure privacy?
Answer
A # field lives on the instance and the engine brand-checks access. A closure keeps the value in the environment and never installs it as a property. Both hide data from casual reads. Only the field shows up on the instance.
Does a closure copy its variables?
Answer
No. It captures the binding. If the binding is one var shared by the loop, every callback sees the final value. let in the for head creates a binding per iteration, so each callback closes over a different one.
What happens if you call a method that was pulled off its object?
Answer
In strict mode this is undefined, and reading a property throws. In sloppy mode this is the global object, which is worse because the write can succeed in the wrong place. Pass a bound function or an arrow that already closed over the receiver you want.
Where do class methods live?
Answer
On Constructor.prototype, not on each instance. Fields and assignments inside the constructor are own properties. A subclass prototype sits in front of the parent prototype. super.speak() walks to the parent on purpose.
What is the temporal dead zone in practice?
Answer
let and const are hoisted as bindings but uninitialized. Touching them before the line that initializes them throws. var would have given you undefined instead, which hides the bug.
What do you say in the first minute?
Answer
Scope is lexical. A closure is the function plus those bindings. var shares one binding. let and const are per block and dead until init. this is new, then bind, then the receiver, then undefined in strict code. Arrows ignore that. class puts methods on the prototype.
Pitfalls
- Closing over
var iand logging it from a later turn. - Passing
obj.methodto a timer or an event and expecting the receiver to come along. - Writing
arrow: () => this.ninside an object literal and calling it a method. - Confusing
Func.prototypewithObject.getPrototypeOf(instance). - Storing per-instance arrows for every method and then wondering why the objects are large.
- Capturing an entire options object when the callback needed one string. The memory page is the follow-up.
Take a one-field object with a concise method. Call it as a method, then detach it and call it under strict mode. Rewrite the method as an arrow and call it both ways. Write one sentence on which form you would pass to setTimeout, and which binding a for loop should use if the timer must print 0, 1, and 2.