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.
On this page
- Words used in this lesson
- Part 1. Objects are bags of properties
- Part 2. The prototype chain
- Part 3. Constructor functions and new
- Part 4. Writing, shadowing and the shared-object trap
- Part 5. Freezing, sealing and hasOwn
- Part 6. for...in against Object.keys
- Try it
- Interview questions
- Key takeaways
- 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
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:
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
42became the string'42'. - Short forms:
{ name, age }means{ name: name, age: age }, andgreet() {}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.keysprinted1,2,b,a. If you need strict insertion order for arbitrary keys, use aMap(see lesson 4).
Property descriptors
Each property has a hidden descriptor with these fields:
| Field | Meaning | Default when you write obj.x = 1 | Default with defineProperty |
|---|---|---|---|
value | the stored value | the value | undefined |
writable | can you assign a new value? | true | false |
enumerable | does it show in Object.keys, for...in, spread? | true | false |
configurable | can you delete it or change its descriptor? | true | false |
Look at one, and then make a locked one:
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:
{ 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:
- A normal property has all three switches on.
definePropertywith onlyvaluegives you the safe defaults: all three switches off.o.id = 99did nothing and said nothing in normal (sloppy) mode. The value stayed7. In strict mode, the same line throws aTypeError. Modules and classes are always strict.- A non-enumerable property is invisible to
Object.keys,JSON.stringifyandconsole.logof the object, but you can still read it by name (o.hiddengave1).
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.
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:
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
The one hidden link
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).
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:
true true walking[ 'barks' ] truetruenulldog has one own property (barks). It can still read eats and call walk() because those live on animal. The chain here is:
dog -> animal -> Object.prototype -> nullHow a read walks the chain
The specification's OrdinaryGet algorithm says, in plain words:
- Look for an own property with this name on the object.
- If it is missing, take the object's prototype. If the prototype is
null, the answer isundefined. - Otherwise, repeat the same lookup on the prototype.
- 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:
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:
1 2 3 undefinedC -> B -> A -> (empty)c.C: found oncat once.c.B: not onc, found onb.c.A: not onc, not onb, found ona.c.D: not onc,b,a, nor onObject.prototype(the last one shows as "(empty)" because its methods are not enumerable). Thennullis reached, so the result isundefined.
The built-in chain
Even {} has a prototype. That is where toString, hasOwnProperty and friends come from:
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:
[Object: null prototype] { a: 1 } undefinedfunction true falseObject.create(null)makes an object with no prototype at all. It has notoString. This is useful as a pure dictionary, because no key can collide with an inherited name."toString" in plainistruebecause theinoperator checks the whole chain.Object.hasOwn(plain, "toString")isfalsebecause 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.
| Name | What it is | Who has it |
|---|---|---|
[[Prototype]] | the hidden link itself | every 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.prototype | a normal property on a function. It is the object that new Fn() will use as the prototype of new objects | ordinary 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:
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:
Rex says wooftruetruetrue truetrue truetrue true true[ 'name' ] true false- Every ordinary function gets a
prototypeproperty, and that object starts with one property,constructor, pointing back to the function. d.constructorworks only becauseddoes not have its ownconstructor, so the lookup finds the one onDog.prototype.- Methods are stored once on
Dog.prototypeand shared by every dog. Data (name) is stored on each object. This saves memory. instanceofasks: "isDog.prototypesomewhere on the chain ofd?" It does not look at who created the object.
What new does, step by step
new Dog("Rex") does three things:
- Create a new empty object, with
Dog.prototypeas its[[Prototype]]. - Call
Dogwiththisset to that new object. - If
Dogreturns an object, that is the result ofnew. Otherwise (no return, or a primitive), the result is the new object from step 1.
We can write that ourselves and test it:
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:
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
newruns it as a normal function. In sloppy modethisis the global object, sothis.n = ncreated a global variable, andForgot("x")returnedundefined. That is why constructors are named with a capital letter, and why classes (which throw if called withoutnew) 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.
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:
1 0 [ 'x' ] [ 'x' ]true false0 [ 'x' ]1 falseTrace:
a.count++meansa.count = a.count + 1. The read finds0onbase. The write creates an owncountona. This is shadowing:a.countnow hidesbase.count, which is still0.a.tags.push("x")first readsa.tags. That finds the array onbase. Thenpushchanges that one shared array. No own property was created. A new objectbsees the change too.- The last case shows the exception: the inherited property
kis read-only, solocked.k = 2did nothing (silently, in sloppy mode), and no ownkwas 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:
| Call | Add properties | Delete properties | Change values | Check |
|---|---|---|---|---|
Object.preventExtensions(o) | no | yes | yes | Object.isExtensible |
Object.seal(o) | no | no | yes | Object.isSealed |
Object.freeze(o) | no | no | no | Object.isFrozen |
"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:
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{} falsefreezeis shallow.cfg.nested.debugchanged, 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
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:
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: badWhich 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.valuesorObject.entrieswith a normalfor...ofloop. They look at own enumerable string keys only. for...inincludes inherited enumerable properties. That is howbadleaked into the array loop. Do not usefor...inon arrays; usefor...oforforEach.
Try it
- Predict:
const o = Object.create({ x: 1 }); o.y = 2;What areObject.keys(o).lengthand"x" in o? - Fill the blank.
Object.getPrototypeOf([]) === ____andObject.getPrototypeOf(Object.getPrototypeOf([])) === ____. - What does
JSON.stringify(Object.create({ a: 1 }))return, and what does.agive on that object? - Why is
Dog.prototype.__proto__ === Object.prototypetrue butDog.__proto__ === Object.prototypefalse?
Answers:
1andtrue. I ran:Object.keysgave a length of 1 and"x" in ogavetrue.Array.prototype, thenObject.prototype(I ran both checks:true).- It returns
"{}"becauseJSON.stringifyonly writes own enumerable properties..ais still1, found through the chain. I ran it:{}and1. Dog.prototypeis a plain object created forDog, so its prototype isObject.prototype.Dogitself is a function, so its prototype isFunction.prototype. In theDogrun above,Dog.prototype.__proto__ === Object.prototypeandDog.__proto__ === Function.prototypeboth printedtrue.
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 untilnull. Writes make own properties. Fn.prototypeis the prototype of objects made bynew Fn(). It is not the prototype ofFn. UseObject.getPrototypeOf, not__proto__.new= create an object fromFn.prototype, runFnwiththis= that object, return an object.- Shadowing creates an own property. Mutating an inherited array or object changes the shared one.
- Descriptors control
writable,enumerableandconfigurable.freezeandsealare shallow. - Use
Object.hasOwn, andObject.keysorentriesfor data. Avoidfor...inunless 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