Overview
Closures and prototypes are essential to JavaScript’s identity as a language. Closures enable powerful functional programming patterns, while prototypes provide a flexible inheritance mechanism.Understanding closures and prototypes deeply is crucial for implementing a JavaScript engine and writing efficient JavaScript code.
Closure implementation with environment chains
Closures allow functions to access variables from their lexical scope even after the outer function has returned. This is implemented using environment chains.What is a closure?
A closure is a function bundled together with references to its surrounding state (lexical environment).Environment chains
Environments form a chain from the innermost scope to the global scope:1
Create environment
When a function is called, create a new environment containing its local variables and parameters.
2
Link to parent
Set the environment’s parent to the lexical parent scope (not the dynamic caller).
3
Variable lookup
Search for variables by traversing the chain from inner to outer environments.
4
Capture references
Functions store a reference to their creation environment, not a copy.
Implementation
Upvalues optimization
To avoid keeping entire environment chains alive, use upvalues to capture only needed variables:Lua pioneered the upvalue technique, which is more memory-efficient than capturing full environments.
Prototype chain traversal
JavaScript uses prototype-based inheritance instead of class-based inheritance. Every object has a prototype, and property lookups traverse the prototype chain.Prototype basics
Prototype chain structure
- Object structure
- Property lookup
- Property assignment
Every object contains:
- Own properties (stored directly on the object)
- A hidden [[Prototype]] link to another object
Implementation
Property lookup optimization
Prototype chain traversal can be slow. Real JavaScript engines use sophisticated optimizations.Inline caching
Cache property locations to avoid repeated lookups:V8’s “hidden classes” and SpiderMonkey’s “shapes” use this technique to make property access as fast as C struct field access.
Polymorphic inline caching
Handle multiple object shapes at the same call site:- Monomorphic
- Polymorphic
- Megamorphic
Single object shape observed:
Best practices
Minimize closure size
Only capture variables actually used by the closure to reduce memory usage
Avoid modifying prototypes
Changing prototypes at runtime invalidates inline caches and hurts performance
Consistent object shapes
Initialize objects with all properties in the same order for better optimization
Prefer composition
Use composition over deep inheritance chains for better performance and maintainability
Common pitfalls
Closure memory leaks
Closure memory leaks
Closures keep their entire environment alive, which can prevent garbage collection:
Prototype pollution
Prototype pollution
Modifying Object.prototype affects all objects:
Shadowing prototype properties
Shadowing prototype properties
Own properties shadow prototype properties:
Next steps
Parsing and AST
Learn about lexical analysis, tokenization, and AST construction
Bytecode and VM
Design bytecode and implement a stack-based virtual machine