Prototypes and the prototype chain
Explain how property lookup follows the prototype chain, and create, inspect and share behaviour through prototypes with Object.create and constructor functions.
What you will learn
- Explain how JavaScript looks up a property along the prototype chain
- Create objects with a chosen prototype using Object.create and read it with Object.getPrototypeOf
- Tell own properties from inherited ones and predict shadowing
- Share methods through a constructor function's prototype property
On this page
In the objects lesson you called list.push(3) and user.toString() without asking where those methods live. An array literal has no push property of its own. The method is found because every object keeps a hidden link to another object, called its prototype, and a lookup that fails on the object itself continues on the prototype, then on the prototype’s prototype, until it either finds the name or reaches null. That sequence of links is the prototype chain, and it is the mechanism behind inheritance in JavaScript, including the class syntax of the next lesson.
This lesson is about the mechanism itself, so the examples use plain objects.
Property lookup
Object.create(proto) makes a new empty object whose prototype is proto. Read the prototype of any object with Object.getPrototypeOf.
const animal = { sound: "...", speak() { return `${this.name} says ${this.sound}`; },};
const dog = Object.create(animal);dog.name = "Rex";dog.sound = "woof";
console.log(dog.speak());Rex says woofdog has no speak, so the lookup goes up to animal and finds it there. The call is written dog.speak(), so by the implicit rule from the previous lesson, this is dog, and the method reads dog’s own name and sound. Had dog not set sound, the lookup would have found animal.sound, "...".
console.log(Object.getPrototypeOf(dog) === animal);console.log(dog);true{ name: 'Rex', sound: 'woof' }getPrototypeOf confirms the link. The logged object shows only its own properties: the inherited speak is real but not listed, since the console prints what the object owns.
Older code reaches the prototype through a property called __proto__. It is deprecated, so use Object.getPrototypeOf to read it and Object.create to choose it. Object.setPrototypeOf can change a prototype after the fact, but it is rarely the right tool: decide the prototype when you create the object.
Own and inherited properties
A property is own if it sits on the object itself, and inherited if the lookup finds it further up. Several tools treat the two differently.
const base = { kind: "base", shared() {} };const child = Object.create(base);child.own = 1;
console.log(Object.hasOwn(child, "own"), Object.hasOwn(child, "kind"));console.log("own" in child, "kind" in child, "missing" in child);console.log(Object.keys(child));
for (const key in child) { console.log("for...in:", key);}true falsetrue true false[ 'own' ]for...in: ownfor...in: kindfor...in: sharedObject.hasOwn(obj, key)asks about own properties only.- The
inoperator searches the whole chain. Object.keys(andentriesandvalues) list own enumerable properties.for...inalso walks the chain and includes inherited enumerable ones, which is the reason lesson 14 told you to preferfor...ofwithObject.keysorObject.entries.
Shadowing
Reading searches the chain. Writing does not: an assignment always creates or changes an own property on the object you assigned to. An own property with the same name as an inherited one is said to shadow it.
const defaults = { colour: "grey", size: 10 };const widget = Object.create(defaults);
console.log(widget.colour);widget.colour = "red";console.log(widget.colour, defaults.colour);
delete widget.colour;console.log(widget.colour);
defaults.size = 12;console.log(widget.size);greyred greygrey12widget.colour first finds the inherited "grey". Assigning "red" puts a new own property on widget and leaves defaults alone. Deleting it uncovers "grey" again, because the lookup is repeated on every read. The last two lines show that the link is live: changing defaults.size later changes what widget.size finds, since widget never copied it.
The chain of a built-in
Every built-in value sits on a chain too. An array’s prototype is Array.prototype, which holds map, push and the other array methods. Its prototype is Object.prototype, which holds toString and hasOwnProperty. The prototype of Object.prototype is null, which ends the chain.
const list = [1, 2];const arrayProto = Object.getPrototypeOf(list);const objectProto = Object.getPrototypeOf(arrayProto);
console.log(arrayProto === Array.prototype);console.log(objectProto === Object.prototype);console.log(Object.getPrototypeOf(objectProto));truetruenullconsole.log(Object.hasOwn(list, "map"), "map" in list);console.log(Object.hasOwn(arrayProto, "map"));console.log(Object.hasOwn(objectProto, "hasOwnProperty"));false truetruetruelist owns 0, 1 and length, and no map. The method is found one step up. hasOwnProperty is another step above that. In prose the chain of [1, 2] reads:
[1, 2] -> Array.prototype -> Object.prototype -> nullThis is also why functions and objects share methods like toString: their chains meet at Object.prototype.
Constructor functions and the prototype property
Lesson 1 showed new building an object from a constructor function. There is one more thing new does: it sets the new object’s prototype to the function’s prototype property. Every ordinary function has one, starting out as an object that holds a single constructor property pointing back at the function. Put shared methods on that object, and every instance finds them by lookup.
function Person(name) { this.name = name;}
Person.prototype.greet = function () { return `Hi, I am ${this.name}`;};
const ada = new Person("Ada");const grace = new Person("Grace");
console.log(ada.greet(), "/", grace.greet());console.log(ada.greet === grace.greet);console.log(Object.getPrototypeOf(ada) === Person.prototype);console.log(ada.constructor === Person, Object.hasOwn(ada, "constructor"));console.log(ada);Hi, I am Ada / Hi, I am Gracetruetruetrue falsePerson { name: 'Ada' }Both people use the same greet function: ada.greet === grace.greet. Each instance stores only its own data (name), and the method is stored once. That is the reason methods go on the prototype instead of being created in the constructor. ada.constructor works because the lookup finds constructor on Person.prototype, not because ada owns it, and instanceof from lesson 1 asks whether Person.prototype appears in the chain of ada.
Notice the function keyword on greet. An arrow would not get the instance as this, so methods that use this must be regular functions or shorthand methods.
Objects with no prototype
Object.create(null) makes an object at the end of its own chain: nothing is inherited, not even toString. That makes it a safe dictionary for keys you do not control. You saw the same thing in the result of Object.groupBy in lesson 29.
const plain = {};const bare = Object.create(null);
console.log("toString" in plain, "toString" in bare);console.log(Object.getPrototypeOf(bare));
bare.hasOwnProperty = "just data";console.log(bare.hasOwnProperty);console.log(Object.keys(bare));true falsenulljust data[ 'hasOwnProperty' ]A key called hasOwnProperty is just data here. On a normal object, assigning to that name would shadow the real method and break any code that called it. The cost is that such an object lacks toString and friends, so it cannot be put in a template literal directly. Use it for lookup tables and keep it away from code that expects an ordinary object.
Common pitfalls
- Putting mutable data on a prototype. An array or object stored on the prototype is shared by every instance, so one instance’s
pushshows up in all of them. Keep per-instance data in the constructor and put only functions (and constants) on the prototype. - Expecting assignment to change the prototype.
child.x = 1creates an own property. It never writes to the prototype, even when the prototype has anx. - Using
__proto__. It is deprecated. UseObject.getPrototypeOfandObject.create. - Using an arrow function as a prototype method. It cannot see the instance as
this. - Adding methods to
Object.prototypeorArray.prototype. Every object in the program sees them, which breaksfor...inloops and surprises other code. Leave built-in prototypes alone. - Checking a key with
obj.keyon an object from outside. The key may be inherited. UseObject.hasOwn(obj, key)when it must be own.
Exercises
Write chain(value) that returns the names of the objects on the prototype chain of value, nearest first, stopping before null. A name is the constructor.name of that prototype object. Use Object.getPrototypeOf in a loop. Print the chain for [], new Map(), () => {}, 5 and Object.create(null). The expected output is [ 'Array', 'Object' ], [ 'Map', 'Object' ], [ 'Function', 'Object' ], [ 'Number', 'Object' ] and [].
Show solution
Start with the prototype of value and keep stepping up until it is null. A number works too, because Object.getPrototypeOf accepts primitives and returns the prototype of their wrapper type. The last case has no prototype at all, so the loop never runs.
function chain(value) { const names = []; let proto = Object.getPrototypeOf(value); while (proto !== null) { names.push(proto.constructor.name); proto = Object.getPrototypeOf(proto); } return names;}
console.log(chain([]));console.log(chain(new Map()));console.log(chain(() => {}));console.log(chain(5));console.log(chain(Object.create(null)));[ 'Array', 'Object' ][ 'Map', 'Object' ][ 'Function', 'Object' ][ 'Number', 'Object' ][]Create a vehicle object with wheels: 0 and a describe() method that returns "<name> has <wheels> wheels" using this. Make car, bike and cart with Object.create(vehicle). Give car the name "Civic" and 4 wheels, bike the name "Bike" and 2 wheels, and cart only the name "Cart". Print the three descriptions, then print whether cart owns a wheels property. The expected output is Civic has 4 wheels, Bike has 2 wheels, Cart has 0 wheels and false.
Show solution
cart never assigns wheels, so this.wheels finds the inherited 0, and Object.hasOwn reports that cart does not own it.
const vehicle = { wheels: 0, describe() { return `${this.name} has ${this.wheels} wheels`; },};
const car = Object.create(vehicle);car.name = "Civic";car.wheels = 4;
const bike = Object.create(vehicle);bike.name = "Bike";bike.wheels = 2;
const cart = Object.create(vehicle);cart.name = "Cart";Describe each object, and check whether cart has a wheels property of its own:
console.log(car.describe());console.log(bike.describe());console.log(cart.describe());console.log(Object.hasOwn(cart, "wheels"));Civic has 4 wheelsBike has 2 wheelsCart has 0 wheelsfalseThis constructor has a bug: all baskets end up with the same items.
function Basket() {}Basket.prototype.items = [];Basket.prototype.add = function (item) { this.items.push(item); return this;};With it, new Basket().add("apple") and new Basket().add("pear") both report [ 'apple', 'pear' ]. Fix the code so the first basket holds [ 'apple' ] and the second [ 'pear' ], then print the two item lists on one line, and print whether the two baskets share an array. The expected output is [ 'apple' ] [ 'pear' ] and then false.
Show solution
The single array lived on the prototype, so every basket found and changed the same one. Create the array in the constructor so that each instance owns its own, and keep only the method on the prototype.
function Basket() { this.items = [];}
Basket.prototype.add = function (item) { this.items.push(item); return this;};
const first = new Basket().add("apple");const second = new Basket().add("pear");
console.log(first.items, second.items);console.log(first.items === second.items);[ 'apple' ] [ 'pear' ]falseSummary
- Every object has a prototype, another object or
null. A property lookup walks up this chain until it finds the name or reaches the end. Object.create(proto)sets the prototype.Object.getPrototypeOfreads it. Avoid__proto__.- Writes go to the object itself and shadow inherited properties.
deleteuncovers them again. Object.hasOwnandObject.keyssee own properties.inandfor...insee inherited ones too.new F()links the new object toF.prototype, so methods stored there are shared by all instances. Never put mutable data there.Object.create(null)gives an object with no inherited properties.
Next: classes, a cleaner syntax for the constructor and prototype pattern you just built by hand.