JavaScript Part 2

Objects and the Prototype Chain

Property descriptors, how lookup walks the prototype chain, new traced step by step, shadowing, freeze and seal.

Lesson 0025 min read#objects#prototypes#descriptors

On this page

  1. Words used in this lesson
  2. Part 1. Objects are bags of properties
  3. Part 2. The prototype chain
  4. Part 3. Constructor functions and new
  5. Part 4. Writing, shadowing and the shared-object trap
  6. Part 5. Freezing, sealing and hasOwn
  7. Part 6. for...in against Object.keys
  8. Try it
  9. Interview questions
  10. Key takeaways
  11. What's next

In one line: a JavaScript object is a bag of properties plus one hidden link to another object, called its prototype. When you read a property that is not in the bag, the engine follows that link, then the next one, until it finds the name or reaches null. That one rule explains inheritance, new, classes and many interview traps.

This is the first lesson of the second JavaScript series. It continues JavaScript Core Mastery. I assume you know what a variable is, how the call stack works and what a closure is. I do not repeat those. The next lesson, this, call/apply/bind and classes, builds directly on the prototype chain from here.

If you are preparing for interviews, the matching question lists are JavaScript Core Interview Questions and, for algorithms with objects and maps, the DSA course.

What I ran / checked. Every code block below was run with Node v22.22.0 on Linux, and the output is pasted exactly as printed. Error messages are V8's wording, so other engines print different text. For the lookup rules I read the OrdinaryGet algorithm in the ECMAScript specification source (tc39/ecma262, spec.html, read from the main branch in this session). I did not run anything in a browser.

Words used in this lesson

  • Property is a name (a string or a symbol) with something attached to it: a value, or a getter and setter pair.
  • Own property is a property stored directly on the object. Inherited property is one found further along the prototype chain.
  • Prototype is the object that another object falls back to when a property is missing. Every object has one, or has null.
  • Prototype chain is the line of objects you get by following prototypes: object, its prototype, that one's prototype, and so on until null.
  • Property descriptor is a small object that describes one property: its value (or getter and setter) and three switches (writable, enumerable, configurable).

Part 1. Objects are bags of properties

Creating and reading

JavaScript
const user = { name: "Asha", age: 30, "fav-color": "teal", 42: "answer" };console.log(user.name, user["fav-color"], user[42]);const key = "age";console.log(user[key]);user.city = "Pune";delete user.age;console.log(user);const name = "Ravi", age = 25;const p = { name, age, greet() { return "hi " + this.name; } };console.log(p, p.greet());console.log(Object.keys({ b: 1, a: 2, 2: "x", 1: "y" }));

Output:

Text
Asha teal answer30{ '42': 'answer', name: 'Asha', 'fav-color': 'teal', city: 'Pune' }{ name: 'Ravi', age: 25, greet: [Function: greet] } hi Ravi[ '1', '2', 'b', 'a' ]

Things to notice:

  • Dot notation (user.name) needs a plain name. Bracket notation (user["fav-color"], user[key]) takes any string, including one that is stored in a variable.
  • Keys are always strings or symbols. The key 42 became the string '42'.
  • Short forms: { name, age } means { name: name, age: age }, and greet() {} is a method written short.
  • Order is not insertion order for number-like keys. Integer keys come first in ascending order, then string keys in the order they were added. That is why Object.keys printed 1, 2, b, a. If you need strict insertion order for arbitrary keys, use a Map (see lesson 4).

Property descriptors

Each property has a hidden descriptor with these fields:

FieldMeaningDefault when you write obj.x = 1Default with defineProperty
valuethe stored valuethe valueundefined
writablecan you assign a new value?truefalse
enumerabledoes it show in Object.keys, for...in, spread?truefalse
configurablecan you delete it or change its descriptor?truefalse

Look at one, and then make a locked one:

JavaScript
const o = { x: 1 };console.log(Object.getOwnPropertyDescriptor(o, "x"));Object.defineProperty(o, "id", { value: 7 });console.log(Object.getOwnPropertyDescriptor(o, "id"));o.id = 99;console.log(o.id, Object.keys(o), JSON.stringify(o));(function () {  "use strict";  try { o.id = 99; } catch (e) { console.log(e.constructor.name + ": " + e.message); }})();Object.defineProperty(o, "hidden", { value: 1, enumerable: false, writable: true });console.log(Object.keys(o), o.hidden, o);

Output:

Text
{ value: 1, writable: true, enumerable: true, configurable: true }{ value: 7, writable: false, enumerable: false, configurable: false }7 [ 'x' ] {"x":1}TypeError: Cannot assign to read only property 'id' of object '#<Object>'[ 'x' ] 1 { x: 1 }

Read the trace:

  1. A normal property has all three switches on.
  2. defineProperty with only value gives you the safe defaults: all three switches off.
  3. o.id = 99 did nothing and said nothing in normal (sloppy) mode. The value stayed 7. In strict mode, the same line throws a TypeError. Modules and classes are always strict.
  4. A non-enumerable property is invisible to Object.keys, JSON.stringify and console.log of the object, but you can still read it by name (o.hidden gave 1).

Getters and setters

A property can hold two functions instead of a value. The engine calls the getter when you read, and the setter when you write. This kind is called an accessor property.

JavaScript
const person = {  first: "Ada", last: "Lovelace",  get full() { return this.first + " " + this.last; },  set full(v) { [this.first, this.last] = v.split(" "); },};console.log(person.full);person.full = "Grace Hopper";console.log(person.first, person.last);console.log(Object.getOwnPropertyDescriptor(person, "full"));console.log(person);

Output:

Text
Ada LovelaceGrace Hopper{  get: [Function: get full],  set: [Function: set full],  enumerable: true,  configurable: true}{ first: 'Grace', last: 'Hopper', full: [Getter/Setter] }

full looks like a field from outside but is computed. There is no stored value, so the descriptor has get and set instead of value and writable. (How this works inside the getter is the topic of the next lesson; here it is the object you read from.)

Part 2. The prototype chain

Every object has an internal slot called [[Prototype]]. It holds either another object or null. You cannot see the slot, but you can read it with Object.getPrototypeOf(obj) and create an object with a chosen prototype using Object.create(proto).

JavaScript
const animal = { eats: true, walk() { return "walking"; } };const dog = Object.create(animal);dog.barks = true;console.log(dog.barks, dog.eats, dog.walk());console.log(Object.keys(dog), Object.getPrototypeOf(dog) === animal);console.log(Object.getPrototypeOf(animal) === Object.prototype);console.log(Object.getPrototypeOf(Object.prototype));

Output:

Text
true true walking[ 'barks' ] truetruenull

dog has one own property (barks). It can still read eats and call walk() because those live on animal. The chain here is:

Text
dog  ->  animal  ->  Object.prototype  ->  null

How a read walks the chain

The specification's OrdinaryGet algorithm says, in plain words:

  1. Look for an own property with this name on the object.
  2. If it is missing, take the object's prototype. If the prototype is null, the answer is undefined.
  3. Otherwise, repeat the same lookup on the prototype.
  4. If you find a data property, return its value. If you find an accessor, call its getter with the original object as this.

Trace it with a three-link chain:

JavaScript
const a = { A: 1 }, b = Object.create(a), c = Object.create(b);b.B = 2; c.C = 3;console.log(c.A, c.B, c.C, c.D);let o = c, steps = [];while (o) { steps.push(Object.keys(o).join("") || "(empty)"); o = Object.getPrototypeOf(o); }console.log(steps.join(" -> "));

Output:

Text
1 2 3 undefinedC -> B -> A -> (empty)
  • c.C: found on c at once.
  • c.B: not on c, found on b.
  • c.A: not on c, not on b, found on a.
  • c.D: not on c, b, a, nor on Object.prototype (the last one shows as "(empty)" because its methods are not enumerable). Then null is reached, so the result is undefined.

The built-in chain

Even {} has a prototype. That is where toString, hasOwnProperty and friends come from:

JavaScript
const bare = Object.create(null);bare.a = 1;console.log(bare, typeof bare.toString);const plain = {};console.log(typeof plain.toString, "toString" in plain, Object.hasOwn(plain, "toString"));

Output:

Text
[Object: null prototype] { a: 1 } undefinedfunction true false
  • Object.create(null) makes an object with no prototype at all. It has no toString. This is useful as a pure dictionary, because no key can collide with an inherited name.
  • "toString" in plain is true because the in operator checks the whole chain. Object.hasOwn(plain, "toString") is false because it checks own properties only.

An array's chain is array -> Array.prototype -> Object.prototype -> null.

__proto__, prototype and getPrototypeOf

These three names confuse everybody. They are three different things.

NameWhat it isWho has it
[[Prototype]]the hidden link itselfevery object
obj.__proto__an old getter and setter on Object.prototype that reads and writes [[Prototype]]most objects (not Object.create(null) ones)
Object.getPrototypeOf(obj)the modern, official way to read [[Prototype]]works on any object
Fn.prototypea normal property on a function. It is the object that new Fn() will use as the prototype of new objectsordinary functions and classes (not arrow functions)

The key sentence: Fn.prototype is not the prototype of Fn. It is the prototype of the objects that new Fn() creates. The prototype of Fn itself is Function.prototype.

Use Object.getPrototypeOf and Object.setPrototypeOf in new code. __proto__ still works everywhere and appears in old code and tutorials, but the ECMAScript specification tags Object.prototype.__proto__ as "legacy" and optional. Prefer setting the prototype at creation (Object.create, class) instead of changing it later.

Part 3. Constructor functions and new

Before classes existed, this was how you made many similar objects:

JavaScript
function Dog(name) { this.name = name; }Dog.prototype.speak = function () { return this.name + " says woof"; };const d = new Dog("Rex");console.log(d.speak());console.log(Object.getPrototypeOf(d) === Dog.prototype);console.log(d.__proto__ === Dog.prototype);console.log(Dog.prototype.constructor === Dog, d.constructor === Dog);console.log(Dog.__proto__ === Function.prototype, Dog.prototype.__proto__ === Object.prototype);console.log(d instanceof Dog, d instanceof Object, Dog.prototype.isPrototypeOf(d));console.log(Object.keys(d), "speak" in d, Object.hasOwn(d, "speak"));

Output:

Text
Rex says wooftruetruetrue truetrue truetrue true true[ 'name' ] true false
  • Every ordinary function gets a prototype property, and that object starts with one property, constructor, pointing back to the function.
  • d.constructor works only because d does not have its own constructor, so the lookup finds the one on Dog.prototype.
  • Methods are stored once on Dog.prototype and shared by every dog. Data (name) is stored on each object. This saves memory.
  • instanceof asks: "is Dog.prototype somewhere on the chain of d?" It does not look at who created the object.

What new does, step by step

new Dog("Rex") does three things:

  1. Create a new empty object, with Dog.prototype as its [[Prototype]].
  2. Call Dog with this set to that new object.
  3. If Dog returns an object, that is the result of new. Otherwise (no return, or a primitive), the result is the new object from step 1.

We can write that ourselves and test it:

JavaScript
function myNew(Ctor, ...args) {  const obj = Object.create(Ctor.prototype);   // step 1  const result = Ctor.apply(obj, args);        // step 2  return (typeof result === "object" && result !== null) || typeof result === "function" ? result : obj; // step 3}function Point(x, y) { this.x = x; this.y = y; }Point.prototype.sum = function () { return this.x + this.y; };const p = myNew(Point, 2, 3);console.log(p, p.sum(), p instanceof Point);function Weird() { this.a = 1; return { other: true }; }console.log(new Weird(), myNew(Weird));function Prim() { this.a = 1; return 5; }console.log(new Prim());function Forgot(n) { this.n = n; }try { const r = Forgot("x"); console.log(r, globalThis.n); } catch (e) { console.log(e.message); }

Output:

Text
Point { x: 2, y: 3 } 5 true{ other: true } { other: true }Prim { a: 1 }undefined x
  • Returning an object from a constructor replaces the result (Weird). Returning a primitive is ignored (Prim).
  • The last case is the classic bug. Calling a constructor without new runs it as a normal function. In sloppy mode this is the global object, so this.n = n created a global variable, and Forgot("x") returned undefined. That is why constructors are named with a capital letter, and why classes (which throw if called without new) are safer. See lesson 1.

Part 4. Writing, shadowing and the shared-object trap

Reads walk the chain. Writes do not. An assignment obj.x = v creates or changes an own property of obj. It never changes the prototype. There are two small exceptions: if the chain has a setter for x, the setter runs; and if an inherited property is read-only, the assignment is blocked.

JavaScript
const base = { count: 0, tags: [] };const a = Object.create(base);a.count++;                 // read 0 from proto, write 1 on aa.tags.push("x");          // read tags from proto, mutate shared arrayconsole.log(a.count, base.count, a.tags, base.tags);console.log(Object.hasOwn(a, "count"), Object.hasOwn(a, "tags"));const b = Object.create(base);console.log(b.count, b.tags);// read-only on proto blocks assignmentconst locked = Object.create(Object.defineProperty({}, "k", { value: 1, writable: false }));locked.k = 2;console.log(locked.k, Object.hasOwn(locked, "k"));

Output:

Text
1 0 [ 'x' ] [ 'x' ]true false0 [ 'x' ]1 false

Trace:

  1. a.count++ means a.count = a.count + 1. The read finds 0 on base. The write creates an own count on a. This is shadowing: a.count now hides base.count, which is still 0.
  2. a.tags.push("x") first reads a.tags. That finds the array on base. Then push changes that one shared array. No own property was created. A new object b sees the change too.
  3. The last case shows the exception: the inherited property k is read-only, so locked.k = 2 did nothing (silently, in sloppy mode), and no own k was made.

Never put arrays or objects on a prototype as "default data" unless you want every instance to share them. Put mutable data on each instance, in the constructor.

Part 5. Freezing, sealing and hasOwn

Three levels of locking an object:

CallAdd propertiesDelete propertiesChange valuesCheck
Object.preventExtensions(o)noyesyesObject.isExtensible
Object.seal(o)nonoyesObject.isSealed
Object.freeze(o)nononoObject.isFrozen
JavaScript
"use strict";const cfg = Object.freeze({ port: 80, nested: { debug: false } });try { cfg.port = 1; } catch (e) { console.log(e.message); }try { cfg.extra = 1; } catch (e) { console.log(e.message); }cfg.nested.debug = true;console.log(cfg, Object.isFrozen(cfg), Object.isFrozen(cfg.nested));const s = Object.seal({ a: 1 });s.a = 2;try { s.b = 1; } catch (e) { console.log(e.message); }try { delete s.a; } catch (e) { console.log(e.message); }console.log(s, Object.isSealed(s));const ne = Object.preventExtensions({ z: 1 });delete ne.z; console.log(ne, Object.isExtensible(ne));

Output:

Text
Cannot assign to read only property 'port' of object '#<Object>'Cannot add property extra, object is not extensible{ port: 80, nested: { debug: true } } true falseCannot add property b, object is not extensibleCannot delete property 'a' of #<Object>{ a: 2 } true{} false
  • freeze is shallow. cfg.nested.debug changed, because only the top level was frozen. Deep freezing and copying are covered in lesson 6.
  • I ran this file in strict mode (the "use strict" line at the top) so you see the errors. In sloppy mode all the failed writes are silent.

hasOwn, in and hasOwnProperty

Use Object.hasOwn(obj, key) (ES2022) to test for an own property, and key in obj to test the whole chain. The old obj.hasOwnProperty(key) fails on Object.create(null) objects. Prefer Object.hasOwn. It works on every object, including those with no prototype.

Part 6. for...in against Object.keys

JavaScript
const parent = { inherited: 1 };const child = Object.create(parent);child.own = 2;Object.defineProperty(child, "hid", { value: 3, enumerable: false });child[Symbol("s")] = 4;for (const k in child) console.log("for...in:", k);console.log(Object.keys(child));console.log(Object.getOwnPropertyNames(child));console.log(Reflect.ownKeys(child));console.log(Object.entries(child));for (const k in child) { if (Object.hasOwn(child, k)) console.log("own only:", k); }const arr = [10, 20]; Array.prototype.bad = function(){};for (const i in arr) console.log("arr for...in:", i);delete Array.prototype.bad;

Output:

Text
for...in: ownfor...in: inherited[ 'own' ][ 'own', 'hid' ][ 'own', 'hid', Symbol(s) ][ [ 'own', 2 ] ]own only: ownarr for...in: 0arr for...in: 1arr for...in: bad

Which one to use: for...in = own and inherited enumerable string keys; Object.keys, values, entries = own enumerable string keys; Object.getOwnPropertyNames adds non-enumerable ones; Reflect.ownKeys adds symbols too. So:

  • For plain data objects, use Object.keys, Object.values or Object.entries with a normal for...of loop. They look at own enumerable string keys only.
  • for...in includes inherited enumerable properties. That is how bad leaked into the array loop. Do not use for...in on arrays; use for...of or forEach.

Try it

  1. Predict: const o = Object.create({ x: 1 }); o.y = 2; What are Object.keys(o).length and "x" in o?
  2. Fill the blank. Object.getPrototypeOf([]) === ____ and Object.getPrototypeOf(Object.getPrototypeOf([])) === ____.
  3. What does JSON.stringify(Object.create({ a: 1 })) return, and what does .a give on that object?
  4. Why is Dog.prototype.__proto__ === Object.prototype true but Dog.__proto__ === Object.prototype false?

Answers:

  1. 1 and true. I ran: Object.keys gave a length of 1 and "x" in o gave true.
  2. Array.prototype, then Object.prototype (I ran both checks: true).
  3. It returns "{}" because JSON.stringify only writes own enumerable properties. .a is still 1, found through the chain. I ran it: {} and 1.
  4. Dog.prototype is a plain object created for Dog, so its prototype is Object.prototype. Dog itself is a function, so its prototype is Function.prototype. In the Dog run above, Dog.prototype.__proto__ === Object.prototype and Dog.__proto__ === Function.prototype both printed true.

Interview questions

What is the prototype chain? (Must know)

Every object has a hidden link to another object, its prototype. When I read a property, the engine checks the object's own properties first. If the name is missing, it goes to the prototype and repeats, until it finds the name or reaches null, and then the result is undefined. That is how objects share methods without copying them. For example, an array finds map on Array.prototype, and finds hasOwnProperty further up on Object.prototype.

What is the difference between proto and prototype? (Must know)

prototype is a property of constructor functions and classes. It is the object that becomes the [[Prototype]] of instances made with new. __proto__ is an accessor on Object.prototype that reads or writes the [[Prototype]] of any object. So new Dog().__proto__ === Dog.prototype. In new code I use Object.getPrototypeOf instead of __proto__.

What does new do? (Must know)

It creates an empty object whose prototype is Ctor.prototype, calls the constructor with this set to that object, and returns the object, unless the constructor returns another object, in which case that object wins. I can show it with Object.create and apply, as in myNew above.

What is shadowing, and what happens on obj.count++ when count is inherited? (Trap)

The read finds the inherited value. The write creates a new own property on obj. The prototype is not changed. If the inherited value is an array or object and I mutate it (obj.list.push(1)), I do not write to obj at all, so I change the shared one. That is why mutable defaults belong in the constructor.

Follow-up: "How would you spot it?" Object.hasOwn(obj, "list") is false.

How do in, hasOwn and for...in differ? (Must know)

in and for...in look at the whole chain (for...in only enumerable string keys). Object.hasOwn, Object.keys and friends look at own properties only. I prefer Object.hasOwn over obj.hasOwnProperty because it also works on Object.create(null) objects.

What is a property descriptor? (Must know)

It is the hidden description of a property: value, writable, enumerable and configurable for data properties, or get, set, enumerable and configurable for accessors. I read it with Object.getOwnPropertyDescriptor and set it with Object.defineProperty. Defaults differ: normal assignment turns all switches on, defineProperty turns them off.

What is the difference between freeze, seal and preventExtensions? (Must know)

preventExtensions stops adding properties. seal also stops deleting. freeze also makes every existing property read-only. All three are shallow, so nested objects stay changeable. In sloppy mode, failures are silent; in strict mode they throw TypeError.

Can a constructor function return a value? (Trap)

If it returns an object, new returns that object and the new this is thrown away. If it returns a primitive, the primitive is ignored. If I forget new, this is the global object in sloppy mode (or undefined in strict mode, which throws), so the constructor can create globals by mistake.

Key takeaways

  • An object is a set of properties plus one [[Prototype]] link. Reads walk the link until null. Writes make own properties.
  • Fn.prototype is the prototype of objects made by new Fn(). It is not the prototype of Fn. Use Object.getPrototypeOf, not __proto__.
  • new = create an object from Fn.prototype, run Fn with this = that object, return an object.
  • Shadowing creates an own property. Mutating an inherited array or object changes the shared one.
  • Descriptors control writable, enumerable and configurable. freeze and seal are shallow.
  • Use Object.hasOwn, and Object.keys or entries for data. Avoid for...in unless you want inherited keys.

What's next

Next: this, call/apply/bind and classes, Lesson 1. You will see that class is a neater way to write what you just did by hand. Practise: write myNew again from memory, then use it to build objects from a class-like constructor, and check instanceof.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series