Concept
The beginner framing: when you access a property on an object and it's not found directly on that object, JavaScript doesn't just give up, it checks a hidden, linked "parent" object, and then that object's parent, and so on, until it finds the property or runs out of parents.
The precise mental model: every object has an internal [[Prototype]] link (exposed via Object.getPrototypeOf(obj), or the non-standard-but-universal obj.__proto__) pointing to another object. Property lookup walks this chain, the prototype chain, checking the object itself first, then its prototype, then its prototype's prototype, until it reaches null (the end of every chain).
const animal = {
eats: true,
describe() { return `${this.name} eats`; },
};
const dog = Object.create(animal); // dog's [[Prototype]] is `animal`
dog.name = "Rex";
console.log(dog.eats); // true, not on dog, found on animal via the chain
console.log(dog.describe()); // "Rex eats", method found on animal, `this` is still dog
console.log(Object.getPrototypeOf(dog) === animal); // trueThe engine-level view, prototype vs. __proto__ are two different things that are easy to conflate:
Function.prototypeis a regular object property that only exists on functions, it's the object that will become the[[Prototype]]of any instance created withnew SomeFunction().__proto__(orObject.getPrototypeOf) is the actual[[Prototype]]link on any object, including on functions themselves, which are objects too.
function Dog(name) {
this.name = name;
}
Dog.prototype.bark = function () { return `${this.name} says woof`; };
const rex = new Dog("Rex");
rex.bark(); // "Rex says woof"
Object.getPrototypeOf(rex) === Dog.prototype; // true, `new` sets this link automatically
rex.hasOwnProperty("bark"); // false, bark lives on the prototype, not on rex itself
rex.hasOwnProperty("name"); // true, name was set directly in the constructornew Dog("Rex") does three things: creates a new object, sets its [[Prototype]] to Dog.prototype, and runs Dog with this bound to that new object (see this Binding, this is the new binding rule).
class is prototype syntax in a trench coat
class Dog {
constructor(name) {
this.name = name;
}
bark() {
return `${this.name} says woof`;
}
}
// Behind the scenes, this is EXACTLY equivalent to the constructor-function
// version above: bark() is added to Dog.prototype, not to each instance.
typeof Dog; // "function", classes are functions
Object.getPrototypeOf(new Dog("Rex")) === Dog.prototype; // trueclass adds real syntax-level features on top (private fields with #, cleaner super calls, a mandatory new), but the underlying object-and-prototype-chain mechanics are identical to what constructor functions have always done.
Try It
Predict what each line logs.
const base = { greeting: "hello" };
const child = Object.create(base);
child.greeting = "hi";
console.log(child.greeting);
console.log(base.greeting);
console.log(Object.getPrototypeOf(child) === base);
delete child.greeting;
console.log(child.greeting);Solution
"hi", child has its OWN greeting property, shadowing base's
"hello", base is untouched, child's own property never wrote through to base
true
"hello", deleting child's OWN property un-shadows base's, exposing it via the chain againThis is the same shadowing concept from Scope, applied to the prototype chain instead of the scope chain, an object's own property always wins over one found further up the chain, and removing the own property reveals whatever's beneath it.
Implement It Yourself
Implement a simplified myCreate (a stand-in for Object.create) and myNew (a stand-in for the new keyword) to see exactly what each one does:
function myCreate(proto) {
function F() {}
F.prototype = proto;
return new F();
}
function myNew(Constructor, ...args) {
const instance = Object.create(Constructor.prototype); // step 1 + 2: new object, link prototype
const result = Constructor.apply(instance, args); // step 3: run constructor with `this` = instance
return typeof result === "object" && result !== null ? result : instance;
}
The last line of myNew, checking whether the constructor explicitly returned an object, reproduces a genuinely obscure real rule: if a constructor function returns an object, new uses THAT object instead of the freshly created one. Try modifying Dog to return { name: "Override" } and see myNew return that instead of the instance you'd expect.
Common Mistakes
1. Modifying built-in prototypes
Array.prototype.last = function () {
return this[this.length - 1];
}; // works, but pollutes EVERY array in the entire program, including third-party codeThis is called "monkey-patching" a built-in, and it's broadly considered dangerous: it can silently break libraries that iterate for...in over arrays (your added method shows up as an enumerable property), or collide with a future spec addition of the same name. Prefer a standalone utility function instead.
2. Confusing Dog.prototype with dog.__proto__
function Dog() {}
const rex = new Dog();
console.log(Dog.prototype); // the object new-created instances will link to
console.log(rex.__proto__); // rex's ACTUAL [[Prototype]] link
console.log(rex.__proto__ === Dog.prototype); // true, but they are conceptually different things
console.log(rex.prototype); // undefined, rex is an instance, not a constructor; it has no .prototypeOnly functions have a meaningful .prototype property. Every object (including functions) has a __proto__/[[Prototype]] link. These frequently get mixed up by name alone.
3. Forgetting that prototype chain lookups don't create own properties
const base = { count: 0 };
const child = Object.create(base);
child.count++; // reads base.count (0) via the chain, then WRITES an OWN property on child
console.log(child.count); // 1
console.log(base.count); // 0, untouchedReading walks the chain; a plain assignment (including compound assignment like ++ or +=) always creates or updates an own property on the object you assigned to, it never writes through to the prototype.
Best Practices
- Prefer
classsyntax for anything with inheritance in modern code, it's the same mechanism as constructor functions, but harder to get wrong (no forgettingnew, cleanersupercalls, real private fields). - Never modify prototypes of built-ins (
Array.prototype,Object.prototype, etc.) in production or library code. - Use
Object.create(null)for objects meant to be pure key-value maps with zero inherited properties (notoString, no chain lookup surprises), though in modern code,Mapis usually the better tool for that job. - Use
hasOwnProperty(or , the modern equivalent) when you specifically need to know if a property exists directly on an object, not inherited.
Performance Tips
- Prototype chain lookups add a small, real cost per level, but V8 optimizes this heavily via "hidden classes" and inline caches, effectively caching where a property was found so repeated lookups on the same shape are fast. Deep prototype chains (many levels) are rarely a real bottleneck in practice, but it's a reason to avoid unnecessarily deep inheritance hierarchies.
hasOwnProperty/Object.hasOwnchecks are essentially free, don't avoid them for performance reasons; use them whenever correctness requires knowing "own vs. inherited."
