Prototype Chain & `this` in JavaScript: In-Depth Guide

TL;DR:
- The prototype chain links objects via their internal
[[Prototype]]
pointers (accessible as__proto__
), enabling property inheritance. - The
this
keyword is determined by call-site: default/global, implicit (method), explicit (call
/apply
/bind
), ornew
binding—and arrow functions inherit from their enclosing scope. - Understanding both ensures you know where properties come from and what context functions execute in, leading to predictable, maintainable code.
1. Understanding the Prototype Chain
Every JavaScript object has an internal [[Prototype]]
reference, forming a chain that ends at null
. When you access a property:
- JS checks the object itself (
own
property). - If missing, it follows
obj.__proto__
to the next object in the chain. - Continues until the property is found or the chain ends.
Analogy: Think of a family tree—if you don’t find a trait in yourself, you look to your parent, then grandparent, and so on.
const grandparent = { role: 'grandparent' } const parent = Object.create(grandparent) parent.role = 'parent' const child = Object.create(parent) child.name = 'Alice' console.log(child.name) // → "Alice" (own property) console.log(child.role) // → "parent" (inherited from parent) delete parent.role console.log(child.role) // → "grandparent" (inherited up the chain)
Object.create(proto)
setsproto
as the new object’s__proto__
.- Properties closer to the object shadow those higher in the chain.
2. Seeds of the Chain: prototype
on Functions
Constructor functions and ES6 classes use the prototype
property to define methods shared across instances:
function Person(name) { this.name = name } Person.prototype.greet = function () { console.log(`Hi, I'm ${this.name}`) } const bob = new Person('Bob') bob.greet() // → "Hi, I'm Bob" console.log(bob.__proto__ === Person.prototype) // → true
-
When you call
new Person()
, JS:- Creates a fresh object whose
__proto__
points toPerson.prototype
. - Binds
this
insidePerson
to that new object. - Returns the object (unless another is explicitly returned).
- Creates a fresh object whose
ES6 classes work identically under the hood:
class Person { constructor(name) { this.name = name } greet() { console.log(`Hi, I'm ${this.name}`) } }
3. The Four Rules of this
Binding
How does JS decide what this
refers to inside a function? There are four binding rules:
-
Default Binding
- In non-strict mode, unbound calls default
this
to the global object (window
/global
). - In strict mode, it becomes
undefined
.
function foo() { console.log(this) } foo() // → window (non-strict) or undefined (strict)
- In non-strict mode, unbound calls default
-
Implicit Binding
- When called as a method on an object,
this
refers to that object.
const obj = { x: 42, getX() { return this.x }, } obj.getX() // → 42
- When called as a method on an object,
-
Explicit Binding
- Using
call
,apply
, orbind
you can explicitly setthis
.
function show() { console.log(this.name) } const user = { name: 'Carol' } show.call(user) // → "Carol" const bound = show.bind(user) bound() // → "Carol"
- Using
-
new
Binding- When a function is invoked with
new
,this
points to the newly created object.
function User(name) { this.name = name } const dana = new User('Dana') console.log(dana.name) // → "Dana"
- When a function is invoked with
Tip: Arrow functions don’t get their own
this
; they inherit it lexically from the enclosing scope.
4. Common Pitfalls & Best Practices
Pitfall | Why It Breaks | Best Practice |
---|---|---|
Forgetting new | Constructor call without new binds to global/undefined this | Use new.target checks or factory functions |
Losing this in callbacks | Passing methods directly loses implicit binding | Use .bind() , arrow functions, or arrow callbacks |
Deep prototype chains | Long lookup slows property access | Favor composition or mixins over deep inheritance |
Arrow functions as methods | Arrow inherits wrong this | Avoid arrow for object methods; use them for callbacks |
5. Visualizing Prototype & this
Person.prototype ──► { greet() }
▲
new(Person)│ __proto__
alice───────┘
Call: alice.greet()
Binding: implicit → this = alice
Lookup: greet on alice? no → on Person.prototype → invoke
6. Putting It All Together
- Prototype chain gives you inheritance without classes; use
Object.create
or constructor functions to control lookup. this
binding determines function context—choose the right call pattern (obj.method()
,fn.call(thisArg)
,new Fn()
, or arrow functions) to avoid surprises.- Combine clarity (shallow chains, clear binding) with performance (fewer lookups) and maintainability (composition over inheritance).
Master these concepts to write predictable, efficient JavaScript—where you always know where your properties come from and what this
refers to at execution time.