How the JavaScript Prototype Chain Works
Understand JavaScript prototype chain lookup, the difference between prototype and [[Prototype]], how inherited properties are shadowed, and how to inspect object links safely.
When you read a property, JavaScript checks the object’s own properties first, then follows its internal [[Prototype]] link to the next object, and repeats until it either finds the property or reaches null, in which case the read returns undefined.
That rule is simple, but it gets harder to follow once you see prototype, __proto__ and [[Prototype]] next to each other in a DevTools panel or an interview question. Firefox even labels the link <prototype> when you expand an object in the console.
This article covers only how a property lookup walks the chain: which object answers a read, where writes land, and which APIs to use to inspect a prototype. For constructor functions, classes and Object.create inheritance patterns, see A Beginner’s Guide to OOP in JavaScript and Inheritance in JavaScript Object-Oriented Programming.
Key Takeaways
- A property read checks own properties, then each
[[Prototype]]in turn, and returnsundefinedonce the chain ends atnull. - A function’s
prototypeproperty becomes the[[Prototype]]of instances created withnew. It is not the function’s own prototype. __proto__is a deprecated accessor inherited fromObject.prototype. UseObject.getPrototypeOf()andObject.setPrototypeOf()instead.- Assigning to an inherited property usually creates an own property that shadows the inherited one and leaves the prototype unchanged.
Object.create(null)produces an object with no chain, which makes it a safe dictionary for arbitrary string keys.
What Is the JavaScript Prototype Chain Lookup Rule?
The JavaScript prototype chain is the sequence of objects, linked through [[Prototype]], that a property read walks until it finds a match or reaches null. Every ordinary object has an internal [[Prototype]] slot that points to another object or to null. The specification’s OrdinaryGet algorithm defines how a read works. It looks for an own property, and if there isn’t one, it repeats the lookup on the prototype. For most objects, the chain ends at Object.prototype, whose prototype is null.
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (found on animal)
console.log(rabbit.toString === Object.prototype.toString); // true
console.log(rabbit.flies); // undefined (not found anywhere)
console.log(Object.getPrototypeOf(Object.prototype)); // null
// rabbit -> animal -> Object.prototype -> null
A missing property is not an error when you read it. Calling rabbit.flies() is what throws a TypeError, because the call is made on undefined. MDN’s Inheritance and the prototype chain guide covers the same model in more depth.
What Is the Difference Between a Function’s prototype and an Object’s [[Prototype]]?
A function’s prototype property is the object that becomes the [[Prototype]] of instances created with new. It is not the function’s own prototype, which is Function.prototype. To read the real link on any object, use Object.getPrototypeOf().
class Greeter { hello() { return 'hi'; } }
const g = new Greeter();
console.log(Object.getPrototypeOf(g) === Greeter.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Function.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Greeter.prototype); // false
const arrow = () => {};
console.log(arrow.prototype); // undefined
console.log(Greeter.prototype.hello.prototype); // undefined
Arrow functions and plain class methods have no prototype property at all, so reading .prototype on them returns undefined.
| Name | What it is | Where it lives | How to read it |
|---|---|---|---|
[[Prototype]] | The internal link that lookup follows | Every object (an internal slot) | Object.getPrototypeOf(obj) |
obj.__proto__ | A legacy getter/setter for that link | Object.prototype | Avoid; use getPrototypeOf |
Fn.prototype | The object assigned to instances made with new Fn() | Constructible functions and classes | Fn.prototype |
What Is proto?
__proto__ is an accessor property defined on Object.prototype. It is not stored on each object, so an object that doesn’t inherit from Object.prototype doesn’t have it. MDN marks Object.prototype.__proto__ as Deprecated and explains that it is in the standard only so older websites keep working. MDN recommends Object.getPrototypeOf() and Object.setPrototypeOf() instead.
const proto = { greet() { return 'hello'; } };
const obj = Object.create(proto);
console.log(Object.getPrototypeOf(obj) === proto); // true
console.log(Object.hasOwn(obj, '__proto__')); // false
console.log(Object.hasOwn(Object.prototype, '__proto__')); // true
const bare = Object.create(null);
console.log(bare.__proto__); // undefined
const lit = { __proto__: proto };
console.log(Object.getPrototypeOf(lit) === proto); // true
The { __proto__: value } key in an object literal is standard syntax and separate from the accessor. It is not deprecated. Only the colon form sets the prototype. A computed ["__proto__"] key creates an ordinary own property.
Even with the proper API, try not to change a prototype after an object is created. MDN warns that doing so is “currently a very slow operation in every browser and JavaScript engine.” Set the prototype when you create the object instead.
Shadowing: Own Properties Hide Inherited Ones
Lookup stops at the first match, so an own property hides any property with the same name further up the chain. Assignment works the same way. Unless an inherited setter or a non-writable inherited property gets in the way, the specification’s OrdinarySet creates or updates an own property on the target object. The inherited property is shadowed, not changed.
const defaults = { theme: 'light' };
const settings = Object.create(defaults);
console.log(settings.theme); // 'light' (inherited)
settings.theme = 'dark'; // creates an own property
console.log(defaults.theme); // 'light' (prototype untouched)
console.log(Object.hasOwn(settings, 'theme')); // true
delete settings.theme;
console.log(settings.theme); // 'light' (inherited again)
// settings -> defaults -> Object.prototype -> null
Object.hasOwn() tells you which layer answered a read. MDN recommends it in place of hasOwnProperty().
How Do Array.prototype Methods Reach Every Array?
Every array created with an array literal has Array.prototype as its [[Prototype]]. That is why map, filter and push work on every array without being copied onto it. Built-ins rely on shadowing too: Array.prototype.toString hides Object.prototype.toString.
const nums = [1, 2, 3];
console.log(Object.getPrototypeOf(nums) === Array.prototype); // true
console.log(Object.hasOwn(nums, 'map')); // false
console.log(nums.map === Array.prototype.map); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(nums.toString()); // '1,2,3'
console.log(Object.prototype.toString.call(nums)); // '[object Array]'
// nums -> Array.prototype -> Object.prototype -> null
Object.create(null) for Objects With No Chain
Object.create(null) creates an object whose [[Prototype]] is null. It inherits nothing: no toString, no hasOwnProperty, no __proto__ accessor. A null-prototype object is therefore a safe container for keys that come from user input. With a plain {}, inherited names such as constructor already return a value before you store anything.
const words = ['a', 'constructor', 'a'];
const plain = {};
for (const w of words) plain[w] = (plain[w] ?? 0) + 1;
console.log(plain.constructor); // a string like "function Object() { [native code] }1"
const counts = Object.create(null);
for (const w of words) counts[w] = (counts[w] ?? 0) + 1;
console.log(counts.constructor); // 1
console.log(counts.a); // 2
console.log('toString' in counts); // false
// counts -> null
A null-prototype object has no inherited methods, so call Object.hasOwn(counts, key) rather than counts.hasOwnProperty(key). Interpolating it into a template literal throws a TypeError because there is no toString to convert it to a string. When keys aren’t strings, use a Map.
Do Classes Use the Same Prototype Chain?
Yes. Class syntax does not add a new inheritance model. Methods declared in a class body live on ClassName.prototype, and instances reach them through the same [[Prototype]] lookup as any other object. extends links one class’s prototype object to the parent’s.
class Animal { speak() { return 'sound'; } }
class Dog extends Animal { bark() { return 'woof'; } }
const d = new Dog();
console.log(typeof Dog); // 'function'
console.log(Object.hasOwn(d, 'speak')); // false
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(d.speak()); // 'sound'
// d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
Conclusion
Every property read follows the same path. JavaScript checks the object itself, then each [[Prototype]] in turn until it finds the property or reaches null. Writes stay on the object you assign to. Next time a value looks like it came from nowhere, run Object.hasOwn() on the object, then call Object.getPrototypeOf() on each link and write out the chain until you find the object that actually holds the property.
FAQs
What is the difference between the in operator and Object.hasOwn()?
The in operator checks the whole prototype chain, so 'toString' in {} returns true because toString is inherited from Object.prototype. Object.hasOwn(obj, key) checks only the object's own properties and returns false for anything inherited. Use in when an inherited property should count, and Object.hasOwn() when you need to know whether the object itself holds the key.
How does instanceof use the prototype chain?
By default, obj instanceof C returns true if C.prototype appears anywhere in the prototype chain of obj. It does not prove that C created the object, because any object whose chain includes C.prototype passes. A class can replace this check by defining a static Symbol.hasInstance method. Primitive values on the left side always return false with the default check.
Do for...in loops and Object.keys() include inherited properties?
A for...in loop visits enumerable string-keyed properties on the object and on every object up its prototype chain. Object.keys(), Object.entries() and object spread include only the object's own enumerable properties. Methods on built-in prototypes such as Object.prototype and Array.prototype are non-enumerable, so they do not show up in for...in. Enumerable properties on your own prototype objects do.
Can a JavaScript prototype chain contain a cycle?
Not with ordinary objects. Object.setPrototypeOf() throws a TypeError if the new prototype would create a cycle, for example setting a's prototype to b when b already inherits from a. Assigning through the __proto__ setter throws the same error. Because cycles are rejected, a chain of ordinary objects is always finite and ends at null, so a property lookup always stops.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k