What Happens When You Read a Property in JavaScript
Property descriptors, getters, setters and the prototype chain explained through one lookup algorithm, with the shadowing rules that surprise people.
- 8 मिनट में पढ़ें
यह पेज अभी हिन्दी में उपलब्ध नहीं है, इसलिए अंग्रेज़ी मूल पाठ दिखाया जा रहा है।
Every obj.x in JavaScript runs the same small algorithm: check the object’s own properties, then walk up the prototype chain until something answers or the chain ends. Once you can run that algorithm in your head, inheritance, getters, Object.create and most “why is this undefined?” bugs stop being mysterious. This post walks through the lookup step by step, then covers the part people get wrong: assignment does not walk the chain the same way.
Why It Matters
You use this algorithm every time you call a method. [1, 2].map is not stored on your array, it lives on Array.prototype, and JSON.stringify(user) only sees own enumerable properties. Framework internals (Vue’s reactivity, MobX, Immer), proxies, ORMs and test mocks all hook into exactly these steps. When one of them behaves strangely, the explanation is almost always in the lookup rules below.
Every Property Has a Descriptor
A property is more than a name and a value. Each one has a descriptor that says what kind of property it is and what you may do with it:
Object.getOwnPropertyDescriptor({ x: 1 }, 'x');
// { value: 1, writable: true, enumerable: true, configurable: true }There are two kinds:
| Kind | Fields | Reading it | Writing it |
|---|---|---|---|
| Data property | value, writable | Returns value | Changes value if writable |
| Accessor property | get, set | Calls get | Calls set |
Both kinds also carry enumerable (does it show up in Object.keys, for…in and JSON.stringify?) and configurable (can it be deleted or redefined?). Properties created with plain assignment are writable, enumerable and configurable. Properties created with Object.defineProperty default to false for all three, which is a common source of “why can’t I change this?”.
An accessor runs code on every read:
let reads = 0;
const clock = {
get now() {
reads++;
return Date.now();
},
};
clock.now;
clock.now;
reads; // 2The Lookup Algorithm
Reading obj.x (the spec calls it [[Get]]) does this:
Two details in that diagram matter in practice:
- The search stops at the first match. A property on the object hides (shadows) any property with the same name further up.
- Getters receive the original object as
this, not the prototype that holds the getter. That is why one getter on a prototype can serve every instance.
Prototypes Are Just Objects
The prototype is an ordinary object that another object delegates to. Object.create makes the link explicit:
const animal = {
eats: true,
describe() {
return `${this.name} eats: ${this.eats}`;
},
};
const rabbit = Object.create(animal);
rabbit.name = 'Rabbit';
rabbit.describe(); // 'Rabbit eats: true'
Object.keys(rabbit); // ['name']
'eats' in rabbit; // true (searches the chain)
Object.hasOwn(rabbit, 'eats'); // false (own properties only)rabbit: own propertynameanimal:eats,describeObject.prototype:toString,hasOwnProperty…null: the end of the chain
describe lives on animal, but this is rabbit, so this.name finds the own property. That is the whole trick behind methods: one function on the prototype, many objects using it. (How this gets its value is a topic of its own: see JavaScript’s this keyword, four rules and one exception.)
Classes Use the Same Machinery
class Account {
deposit() {}
}
const account = new Account();
Object.getPrototypeOf(account) === Account.prototype; // true
Object.keys(Account.prototype).length; // 0: class methods are non-enumerablenew Account() creates an object whose prototype is Account.prototype, and deposit is found there. extends adds one more link: Child.prototype’s prototype is Parent.prototype.
Objects With No Prototype
Object.create(null) makes an object with no chain at all, so nothing is inherited, not even toString:
const dictionary = Object.create(null);
'toString' in dictionary; // false
'toString' in {}; // trueThat makes it a safe dictionary for arbitrary keys like "__proto__" or "constructor". For most key-value data, a Map is the better tool.
Writing Is Not the Mirror of Reading
Assignment ([[Set]]) also consults the chain, but only to decide whether to create an own property:
Situation when you write obj.x = 1 | Result |
|---|---|
obj has its own writable x | Updates it |
Nothing called x anywhere on the chain | Creates an own property on obj |
An inherited data property x is writable | Creates an own property that shadows it |
An inherited data property x is read-only | Fails (a TypeError in strict mode, which modules always are) |
An inherited accessor x has a setter | Calls the setter; no own x is created |
The last two rows surprise people:
const base = Object.defineProperty({}, 'id', { value: 1, writable: false });
const item = Object.create(base);
item.id = 2; // TypeError in strict mode: the inherited id is read-only
Object.defineProperty(item, 'id', { value: 2 }); // defining (not assigning) works
item.id; // 2
base.id; // 1
const proto = {
set name(value) {
this._name = value.toUpperCase();
},
};
const person = Object.create(proto);
person.name = 'ada';
Object.keys(person); // ['_name']: the setter ran, no own "name" was createdWhere the Lookup Leaks Into Real Code
for…inwalks the chain;Object.keys, spread andJSON.stringifysee only own enumerable properties. PreferObject.keysorObject.entriesunless you really want inherited keys.invsObject.hasOwn.inanswers “can I read it?”,Object.hasOwnanswers “is it mine?”. Validation code usually wants the second.- Prototype pollution. Merging untrusted JSON with a naive deep merge lets
{"__proto__": {"isAdmin": true}}write toObject.prototype, and then every object “has”isAdmin.- Defense: reject the keys
__proto__,constructorandprototypewhen merging, or merge intoObject.create(null)or aMap. - Not theoretical: Lodash’s
defaultsDeephad exactly this flaw (CVE-2019-10744), in a library installed millions of times a week; it was fixed in version 4.17.12.
- Defense: reject the keys
- Performance. Engines such as V8 give objects with the same layout a shared hidden “shape” and cache lookups per shape. Create objects with the same properties in the same order, and avoid
Object.setPrototypeOfon existing objects ordeletein hot paths.
Going Deeper: How Engines Keep Lookups Fast
Walking a prototype chain for every property read sounds slow, yet property access in modern engines is usually a single memory load. Two ideas make that possible.
Shapes (hidden classes). An engine such as V8 doesn’t store each object as a dictionary. It gives every object a hidden shape that records which properties it has, in which order, and where each one lives in memory. Objects created the same way share a shape:
Inline caches. The first time a line like user.name runs, the engine does the full lookup and remembers the answer for that shape: “for objects of shape S2, name is at offset 1.” Next time an object of the same shape passes through, it skips the lookup entirely. A line that only ever sees one shape is monomorphic and very fast; one that sees many shapes becomes megamorphic and falls back to slower generic lookups.
What this means in practice, without micro-optimizing:
- Initialize all fields in the constructor, in the same order. Objects created through one constructor or factory then share a shape. Adding properties later, conditionally, creates many shapes.
- Avoid
deleteon hot objects. Deleting a property often switches the object to slow “dictionary mode”. Set the property toundefined, or use aMapfor data that really changes keys. - Don’t change prototypes after creation.
Object.setPrototypeOfinvalidates cached lookups for the affected objects; MDN warns that it is slow in every engine. Choose the prototype withObject.createor a class up front. - Use
Mapfor dictionaries. Objects are optimized for a fixed set of known keys. AMapis built for keys that come and go, such as caches and lookups by id.
These rules matter in hot paths such as rendering lists or parsing large inputs. Elsewhere, clear code wins; measure before you optimize.
Which Tool to Reach for
| You want | Use |
|---|---|
| Shared behavior across many objects | A class or Object.create(proto) |
| A computed or validated property | A getter and setter (get/set) |
| A constant that can’t be reassigned | Object.defineProperty with writable: false, or Object.freeze |
| A dictionary with arbitrary string keys | Map, or Object.create(null) |
| To check ownership | Object.hasOwn(obj, key) |
| To intercept every access | A Proxy (use sparingly: it is slower and harder to debug) |
Test Yourself
Predict the output, then open the answer. Every answer was checked by running the code in an ES module (strict mode).
चुनौती 1
An Inherited Method
const base = { greet() { return 'hi from ' + this.name; } };
const user = Object.create(base);
user.name = 'Ada';
console.log(user.greet(), Object.keys(user), 'greet' in user);उत्तर देखेंउत्तर छिपाएँ
hi from Ada [ 'name' ] true
greetis found on the prototype, butthisis the object before the dot,user, sothis.nameis'Ada'.Object.keyslists only own enumerable properties, sogreetisn’t there.inalso searches the prototype chain, so it findsgreet.
चुनौती 2
Incrementing an Inherited Number
const proto = { count: 0 };
const a = Object.create(proto);
a.count++;
console.log(a.count, proto.count, Object.hasOwn(a, 'count'));उत्तर देखेंउत्तर छिपाएँ
1 0 true
a.count++is a read followed by a write.- The read finds
0on the prototype; the write creates a new own property ona, which shadows the inherited one. The prototype is untouched. - This is why shared state on prototypes surprises people: reads are shared, writes are not.
चुनौती 3
A Read-Only Property Up the Chain
const proto = Object.defineProperty({}, 'id', { value: 1, writable: false });
const obj = Object.create(proto);
try {
obj.id = 2;
} catch (error) {
console.log(error.constructor.name);
}
console.log(obj.id, Object.hasOwn(obj, 'id'));उत्तर देखेंउत्तर छिपाएँ
TypeError, then 1 false
- An assignment checks the chain before creating an own property.
- It finds a non-writable
id, so the write is refused: a throw in strict mode, a silent failure in sloppy mode. - Only
Object.defineProperty(obj, 'id', …)can still add an ownid.
चुनौती 4
A Setter on the Prototype
const proto = {
set name(value) {
this._name = value.toUpperCase();
},
};
const o = Object.create(proto);
o.name = 'ada';
console.log(o._name, Object.keys(o));उत्तर देखेंउत्तर छिपाएँ
ADA [ '_name' ]
- When the chain contains a setter, assignment calls the setter instead of creating an own property, with
thisset too. - So
ogets_name, but nevername. - Frameworks use the same mechanism for reactive properties.
चुनौती 5
Three Ways to List Keys
const parent = { inherited: 1 };
const child = Object.create(parent);
child.own = 2;
const seen = [];
for (const key in child) seen.push(key);
console.log(seen, Object.keys(child), JSON.stringify(child));उत्तर देखेंउत्तर छिपाएँ
[ 'own', 'inherited' ] [ 'own' ] {"own":2}
for…inwalks enumerable properties including inherited ones.Object.keysandJSON.stringifyonly see own properties.- That difference is why copying objects with
for…incan pick up unexpected keys, and why prototype pollution spreads.
चुनौती 6
Methods vs Arrow Fields
class Widget {
render() {}
onClick = () => {};
}
const a = new Widget();
const b = new Widget();
console.log(a.render === b.render, a.onClick === b.onClick, Object.keys(a));उत्तर देखेंउत्तर छिपाएँ
true false [ 'onClick' ]
- Methods live once on
Widget.prototypeand are shared. - Class fields are created on each instance, so every widget gets its own
onClickfunction. - That costs memory per instance but keeps
thisbound, a trade-off covered in JavaScript’s this keyword.
Key Takeaways
- Reading a property checks the object, then each prototype in turn, and returns the first match or
undefined. - Every property is either a data property or an accessor, and its descriptor decides whether it is writable, enumerable and configurable.
- Getters and setters on a prototype run with
thisset to the object you used, which is why methods work. - Assignment usually creates an own property, except when the inherited property is read-only or has a setter.
- Classes are syntax over the same prototype chain;
Object.create(null)gives you an object with no chain at all.
Want to test yourself? The JavaScript round of “What does this print?” has several puzzles built on these rules. The next post in this series looks at the other half of the object model: the hidden cost of mutable state.
Sources
- Mathias Bynens and Benedikt Meurer: JavaScript engine fundamentals: Shapes and Inline Caches
- MDN: Object.setPrototypeOf()
- ECMAScript specification: OrdinaryGet and OrdinarySet
- MDN: Inheritance and the prototype chain
- MDN: Object.defineProperty()
- V8 blog: Fast properties in V8
- Snyk: Prototype pollution
टैग
अक्सर पूछे जाने वाले सवाल
What is the difference between __proto__ and prototype?
Every object has an internal prototype link, which Object.getPrototypeOf reads and __proto__ exposes as a legacy accessor. The prototype property exists only on functions and classes. It is the object that becomes the prototype of instances created with new.
Are JavaScript classes real classes?
NoNot in the classical sense. Class syntax creates a constructor function and puts methods on its prototype object, so instances still find methods through the prototype chain. The syntax adds real features, such as private fields, strict mode and non-enumerable methods, but the object model underneath is prototypes.
Is a long prototype chain slow?
NoEngines cache lookups per object shape, so ordinary chains are fast. What slows code down is changing an object’s prototype after creation or constantly adding and deleting properties, because that invalidates the caches.
Why is delete considered slow?
In engines like V8, deleting a property often moves the object out of its fast hidden shape into a slower dictionary representation, and lookups on it can no longer be cached. Set the property to undefined, or use a Map for data whose keys change.
What is prototype pollution?
An attack where untrusted input with a __proto__ key is merged into an object and ends up writing to Object.prototype, so every object suddenly has extra properties. Reject the keys __proto__, constructor and prototype when merging, or merge into Object.create(null) or a Map.
Should I use Object.create(null) or a Map for a dictionary?
Prefer Map for data with arbitrary or frequently changing keys: it never inherits keys, keeps insertion order, accepts any key type and has a size. Object.create(null) is useful when you need a plain object, for example for JSON output, without inherited properties.
Why does for…in show properties I didn’t add?
for…in includes enumerable inherited properties. Use Object.keys or Object.entries when you only want the object’s own data.
What is the difference between Object.hasOwn and hasOwnProperty?
Both check for an own property. Object.hasOwn(obj, key) is the modern form: it also works on objects created with Object.create(null), which have no hasOwnProperty method, and on objects that override hasOwnProperty.