İçeriğe geç
Clinford Jordano Valencia
Tüm yazılar

JavaScript

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 dk okuma

Bu sayfa henüz Türkçe olarak mevcut değil, bu yüzden İngilizce orijinalini okuyorsunuz.

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:

KindFieldsReading itWriting it
Data propertyvalue, writableReturns valueChanges value if writable
Accessor propertyget, setCalls getCalls 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; // 2

The Lookup Algorithm

Reading obj.x (the spec calls it [[Get]]) does this:

Read obj.xOwn property x on current object?yesData: return valueAccessor: call getterthis = original objnoMove to the prototypePrototype is null?yesundefinedno: check again
Reading a property: check the object, then each prototype in turn, until a property is found or the chain ends at null.

Two details in that diagram matter in practice:

  1. The search stops at the first match. A property on the object hides (shadows) any property with the same name further up.
  2. 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)
  1. rabbit: own property name
  2. animal: eats, describe
  3. Object.prototype: toString, hasOwnProperty…
  4. 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-enumerable

new 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 {}; // true

That 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 = 1Result
obj has its own writable xUpdates it
Nothing called x anywhere on the chainCreates an own property on obj
An inherited data property x is writableCreates an own property that shadows it
An inherited data property x is read-onlyFails (a TypeError in strict mode, which modules always are)
An inherited accessor x has a setterCalls 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 created

Where the Lookup Leaks Into Real Code

  • for…in walks the chain; Object.keys, spread and JSON.stringify see only own enumerable properties. Prefer Object.keys or Object.entries unless you really want inherited keys.
  • in vs Object.hasOwn. in answers “can I read it?”, Object.hasOwn answers “is it mine?”. Validation code usually wants the second.
  • Prototype pollution. Merging untrusted JSON with a naive deep merge lets {"__proto__": {"isAdmin": true}} write to Object.prototype, and then every object “has” isAdmin.
    • Defense: reject the keys __proto__, constructor and prototype when merging, or merge into Object.create(null) or a Map.
    • Not theoretical: Lodash’s defaultsDeep had exactly this flaw (CVE-2019-10744), in a library installed millions of times a week; it was fixed in version 4.17.12.
  • 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.setPrototypeOf on existing objects or delete in 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:

{}{ x }{ x, y }{ y }{ y, x }add xadd yadd yadd xa and b sharethis shapec gets adifferent one
Shapes form a tree of transitions. Objects that get the same properties in the same order share a shape; a different order creates a different 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 delete on hot objects. Deleting a property often switches the object to slow “dictionary mode”. Set the property to undefined, or use a Map for data that really changes keys.
  • Don’t change prototypes after creation. Object.setPrototypeOf invalidates cached lookups for the affected objects; MDN warns that it is slow in every engine. Choose the prototype with Object.create or a class up front.
  • Use Map for dictionaries. Objects are optimized for a fixed set of known keys. A Map is 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 wantUse
Shared behavior across many objectsA class or Object.create(proto)
A computed or validated propertyA getter and setter (get/set)
A constant that can’t be reassignedObject.defineProperty with writable: false, or Object.freeze
A dictionary with arbitrary string keysMap, or Object.create(null)
To check ownershipObject.hasOwn(obj, key)
To intercept every accessA 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).

Görev 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);
Cevabı göster

hi from Ada [ 'name' ] true

  • greet is found on the prototype, but this is the object before the dot, user, so this.name is 'Ada'.
  • Object.keys lists only own enumerable properties, so greet isn’t there.
  • in also searches the prototype chain, so it finds greet.

Görev 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'));
Cevabı göster

1 0 true

  • a.count++ is a read followed by a write.
  • The read finds 0 on the prototype; the write creates a new own property on a, which shadows the inherited one. The prototype is untouched.
  • This is why shared state on prototypes surprises people: reads are shared, writes are not.

Görev 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'));
Cevabı göster

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 own id.

Görev 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));
Cevabı göster

ADA [ '_name' ]

  • When the chain contains a setter, assignment calls the setter instead of creating an own property, with this set to o.
  • So o gets _name, but never name.
  • Frameworks use the same mechanism for reactive properties.

Görev 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));
Cevabı göster

[ 'own', 'inherited' ] [ 'own' ] {"own":2}

  • for…in walks enumerable properties including inherited ones.
  • Object.keys and JSON.stringify only see own properties.
  • That difference is why copying objects with for…in can pick up unexpected keys, and why prototype pollution spreads.

Görev 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));
Cevabı göster

true false [ 'onClick' ]

  • Methods live once on Widget.prototype and are shared.
  • Class fields are created on each instance, so every widget gets its own onClick function.
  • That costs memory per instance but keeps this bound, 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 this set 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

Etiketler

Sık sorulan sorular

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.