JavaScript (ES2026) Module 11 – Prototypes and classes
Prototypes and the prototype chain
How JavaScript objects inherit through their prototype: property lookup along the chain, Object.create, null-prototype objects and safe prototype changes.
What you will learn
- Explain [[Prototype]] and how a property lookup walks the prototype chain
- Predict which reads and writes reach a prototype and which stay on the object
- Create objects with a chosen prototype using Object.create, including objects with no prototype
- Inspect and change prototypes safely, and explain why built-in prototypes are left alone
Before you start
What you’ll learn · Before you start
What you will learn
- Explain [[Prototype]] and how a property lookup walks the prototype chain
- Predict which reads and writes reach a prototype and which stay on the object
- Create objects with a chosen prototype using Object.create, including objects with no prototype
- Inspect and change prototypes safely, and explain why built-in prototypes are left alone
Before you start
On this page
Every JavaScript object has a hidden link to one other object, its prototype, or to null. When you read a
property that an object does not have itself, JavaScript follows that link and looks in the prototype, then in the
prototype’s prototype, and so on, until it finds the name or reaches null. That series of linked objects is the
prototype chain, and it is how JavaScript shares behaviour: push, map and toString exist once, on a
prototype, and every array or object linked to it can use them. Writing works differently. An assignment creates or
changes a property on the object itself, which hides a property of the same name further up the chain. Classes,
constructor functions and Object.create all build on these links. This lesson shows the chain in programs that run,
then how to create objects with the prototype you choose, and how to inspect and change prototypes without slowing
your code down or breaking someone else’s.
The hidden link: [[Prototype]]
The specification calls the link an internal slot, [[Prototype]]. Every ordinary object has one, and it holds
either an object or null. It is not a property you can list or loop over; Object.getPrototypeOf(value) reads it.
This program follows the links of some everyday values until it reaches null:
// The prototype chains of everyday values: Object.getPrototypeOf() returns the next object of a chain, and every
// chain ends with null.
function nameOf(proto) {
// A built-in prototype owns a constructor property that points back to its function: Array.prototype.constructor
// is Array, and so on.
return Object.hasOwn(proto, 'constructor') ? `${proto.constructor.name}.prototype` : 'an object';
}
function chain(value) {
const names = [];
let proto = Object.getPrototypeOf(value);
while (proto !== null) {
names.push(nameOf(proto));
proto = Object.getPrototypeOf(proto);
}
names.push('null');
return names.join(' -> ');
}
const values = {
'{ city: "Pune" }': { city: 'Pune' },
'[10, 20]': [10, 20],
'new Map()': new Map(),
'function area() {}': function area() {},
'new Date(0)': new Date(0),
'Object.create(null)': Object.create(null),
"'chai' (a string)": 'chai',
};
for (const [label, value] of Object.entries(values)) {
console.log(`${label.padEnd(22)} ${chain(value)}`);
} Output
{ city: "Pune" } Object.prototype -> null
[10, 20] Array.prototype -> Object.prototype -> null
new Map() Map.prototype -> Object.prototype -> null
function area() {} Function.prototype -> Object.prototype -> null
new Date(0) Date.prototype -> Object.prototype -> null
Object.create(null) null
'chai' (a string) String.prototype -> Object.prototype -> null
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node builtin-chains.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- An object literal is linked to
Object.prototype, whose own prototype isnull. That is where nearly every chain ends, which is why almost every object hastoStringandhasOwnProperty. - Arrays, Maps, functions and Dates have one more step: their own built-in prototype, which holds
push,get,callorgetFullYear, linked in turn toObject.prototype. Object.create(null)makes an object whose prototype isnullfrom the start. It inherits nothing.- A string is a primitive and has no slot of its own. When you ask for its prototype, or call a method such as
'chai'.toUpperCase(), JavaScript wraps the string in a temporaryStringobject, whose prototype isString.prototype.
Each built-in prototype has a constructor property that points back to its function, so
Array.prototype.constructor is Array; the program uses that to print the names. The next lesson of this module
shows how a function of your own gets such a prototype object, and how new links new objects to it.
Walking the chain
A chain can be as long as you like. Here three objects of our own are linked with Object.create, which makes a new
object with the prototype you pass it, and the program walks from the first object to null, printing what each one
owns:
// Three objects linked by their prototypes, and a walk along the chain from the first one up to null.
const vehicle = {
wheels: 4,
describe() {
return `${this.name} has ${this.wheels} wheels`;
},
};
const twoWheeler = Object.create(vehicle); // a new object whose prototype is vehicle
twoWheeler.wheels = 2;
const scooter = Object.create(twoWheeler);
scooter.name = "Ravi's scooter";
const labels = new Map([
[scooter, 'scooter'],
[twoWheeler, 'twoWheeler'],
[vehicle, 'vehicle'],
[Object.prototype, 'Object.prototype'],
]);
let depth = 0;
for (let obj = scooter; obj !== null; obj = Object.getPrototypeOf(obj)) {
const own = Object.getOwnPropertyNames(obj);
const shown = own.length > 3 ? `${own.length} properties, toString among them` : own.join(', ');
console.log(`${' '.repeat(depth)}${labels.get(obj)} owns: ${shown}`);
depth += 1;
}
console.log(`${' '.repeat(depth)}null: the end of the chain`);
console.log(scooter.describe()); // found two levels up, and this is still scooter
console.log(scooter.wheels); // twoWheeler's 2 is found first, so vehicle's 4 is never reached
console.log(scooter.colour); // looked for at every level and found nowhere
console.log(String(scooter)); // String() calls toString, found on Object.prototype three levels up Output
scooter owns: name
twoWheeler owns: wheels
vehicle owns: wheels, describe
Object.prototype owns: 12 properties, toString among them
null: the end of the chain
Ravi's scooter has 2 wheels
2
undefined
[object Object]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node chain-walk.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The chain a lookup on scooter follows
Text description of the diagram
The diagram shows the objects that `chain-walk.mjs` links together, from top to bottom. Each arrow is a `[[Prototype]]` link.
- `scooter` owns one property, `name`. Its prototype is `twoWheeler`. - `twoWheeler` owns `wheels`, set to 2. Its prototype is `vehicle`. - `vehicle` owns `wheels`, set to 4, and the method `describe()`. Its prototype is `Object.prototype`. - `Object.prototype` owns the methods nearly every object inherits, such as `toString()` and `hasOwnProperty()`. Its prototype is `null`, where the chain ends.
A lookup starts at the top and stops at the first box that owns the name: `scooter.wheels` stops at `twoWheeler` and gives 2, so `vehicle`'s 4 is never reached; `scooter.describe` is found two links down, in `vehicle`; a name that no box owns gives `undefined` once the lookup reaches `null`.
A property lookup does what the loop does. It checks the object’s own properties first. If the name is missing, it moves to the prototype and checks again, and the first object that owns the name supplies the value:
scooter.wheelsis found ontwoWheeler, so the 4 onvehicleis never reached. A property lower in the chain shadows one of the same name higher up.scooter.describe()is found onvehicle, two links up. Inside it,thisis stillscooter, the object the method was called on, so onedescribeserves every object that inherits it. That is the rule the lesson onthisdescribed: the call decides, not the place where the method lives.scooter.colouris found nowhere. Once the lookup reachesnullthe answer isundefined, quietly. A missing variable throws aReferenceError, but a missing property never does.
The specification writes this lookup as the algorithm OrdinaryGet: look for an own property; if there is none, ask the
prototype, and stop at null. Engines do it much faster than a loop, with caches, but the result is the same.
Reading walks the chain; writing stays on the object
Assignments do not travel up the chain the way reads do. Assigning a property gives the object a property of its own, and the prototype stays as it was:
// Reading a property walks the prototype chain; writing one changes the object itself.
const defaults = { theme: 'light', tags: [] };
const asha = Object.create(defaults);
const ravi = Object.create(defaults);
asha.theme = 'dark'; // creates an own property on asha; defaults is untouched
console.log('1.', asha.theme, ravi.theme, defaults.theme);
delete asha.theme; // removes asha's own property, so the inherited one shows through again
console.log('2.', asha.theme);
// The trap: changing an inherited object is not a write to asha. asha.tags finds defaults.tags, and push changes
// that one shared array, which every object linked to defaults sees.
asha.tags.push('vip');
console.log('3.', ravi.tags, Object.hasOwn(asha, 'tags'));
asha.tags = ['new']; // an assignment, by contrast, gives asha an array of her own
console.log('4.', asha.tags, ravi.tags);
// Two exceptions to "writing stays on the object". An inherited setter runs instead, with this set to the object.
const account = {
set pin(value) {
this.pinLength = String(value).length; // stores on the object the setter was called for
},
};
const meera = Object.create(account);
meera.pin = 4321;
console.log('5.', Object.keys(meera), Object.hasOwn(meera, 'pin'));
// And an inherited read-only property blocks the write. Module code is strict, so the refusal throws.
const limits = Object.defineProperty({}, 'maxUploadMb', { value: 10, enumerable: true });
const upload = Object.create(limits);
try {
upload.maxUploadMb = 50;
} catch (err) {
console.log('6.', err.name, '- maxUploadMb is still', upload.maxUploadMb);
}
Object.defineProperty(upload, 'maxUploadMb', { value: 50, enumerable: true }); // defining an own property still works
console.log('7.', upload.maxUploadMb, limits.maxUploadMb); Output
1. dark light light 2. light 3. [ 'vip' ] false 4. [ 'new' ] [ 'vip' ] 5. [ 'pinLength' ] false 6. TypeError - maxUploadMb is still 10 7. 50 10
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node reads-and-writes.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
asha.theme = 'dark'created an own property onasha.ravianddefaultsstill saylight.delete asha.themeremoved onlyasha’s own property, so the inheritedthemeshowed through again.deletenever reaches into a prototype.- The common mistake:
asha.tags.push('vip')is not an assignment toashaat all. It readsasha.tags, finds the one array that lives ondefaults, and changes that array, soravisees'vip'too. Arrays and objects stored on a prototype are shared by everything that inherits them. - Assigning
asha.tags = ['new']gaveashaan array of her own, which hides the shared one from then on.
There are two exceptions, both visible in the output. If the chain holds a setter for the name, the assignment
calls that setter, with this set to the object you assigned on, instead of creating a property: meera.pin = 4321
ran the inherited setter, which stored pinLength on meera. If the chain holds a read-only data property of
that name, the assignment is refused, which throws a TypeError in strict code, every module included, and does
nothing at all in sloppy scripts. Object.defineProperty is not an assignment, so it can still give the object an own
property that hides the read-only one. The specification spells all of this out in OrdinarySetWithOwnDescriptor.
Objects with a chosen prototype, or with none
Object.create(proto) makes an empty object whose prototype is proto, an object or null; anything else throws a
TypeError. A second argument can define properties with descriptors at the same time, as Object.defineProperties
would. An object literal can do the same with the special key __proto__: { __proto__: proto, id: 7 } sets the
prototype instead of creating a property, as the lesson on object literals showed.
The null prototype has a practical use: tables whose keys come from outside the program. A plain object inherits
toString, valueOf and constructor, and a lookup with one of those names finds a function nobody put in the
table:
// A table of command handlers, looked up with names that come from outside the program.
function run(table, command) {
const handler = table[command];
if (typeof handler !== 'function') return `unknown command "${command}"`;
try {
return String(handler('Asha'));
} catch (err) {
return `${err.name} thrown`;
}
}
const plain = {
greet: (name) => `Namaste, ${name}`,
thank: (name) => `Dhanyavaad, ${name}`,
};
// The same table with no prototype: __proto__: null in a literal sets the prototype instead of creating a key.
const bare = {
__proto__: null,
greet: (name) => `Namaste, ${name}`,
thank: (name) => `Dhanyavaad, ${name}`,
};
for (const command of ['greet', 'dance', 'toString', 'valueOf', 'constructor']) {
console.log(`${command.padEnd(11)} plain: ${run(plain, command).padEnd(26)} bare: ${run(bare, command)}`);
}
// What an object without a prototype does not have: everything Object.prototype gives other objects.
console.log('bare.hasOwnProperty:', typeof bare.hasOwnProperty, '| Object.hasOwn(bare, "greet"):', Object.hasOwn(bare, 'greet'));
try {
console.log(`table: ${bare}`); // a template literal needs toString or valueOf
} catch (err) {
console.log('`${bare}` throws a', err.name);
}
console.log('Object.keys still works:', Object.keys(bare));
console.log('a spread copy has a prototype again:', Object.getPrototypeOf({ ...bare }) === Object.prototype); Output
greet plain: Namaste, Asha bare: Namaste, Asha
dance plain: unknown command "dance" bare: unknown command "dance"
toString plain: [object Undefined] bare: unknown command "toString"
valueOf plain: TypeError thrown bare: unknown command "valueOf"
constructor plain: Asha bare: unknown command "constructor"
bare.hasOwnProperty: undefined | Object.hasOwn(bare, "greet"): true
`${bare}` throws a TypeError
Object.keys still works: [ 'greet', 'thank' ]
a spread copy has a prototype again: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node dictionary.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
With the plain table, the command toString ran Object.prototype.toString and answered [object Undefined],
valueOf threw a TypeError, and constructor called Object('Asha'), which quietly returned a wrapped string, so
the output looked like a real answer. The table made with __proto__: null has only the keys written into it, so
all three are unknown commands, as they should be.
Such an object also lacks everything else that Object.prototype gives: bare.hasOwnProperty is undefined, so use
Object.hasOwn(bare, key), and a template literal or String(bare) throws, because there is no toString or
valueOf to call. Object.keys, Object.entries and JSON.stringify work as usual, and a spread copy is an ordinary
object again. When the keys come from users and you also want a size, insertion order and keys of any type, a Map
is often the better tool; the module on collections compares them.
Inspecting and changing a prototype
Object.getPrototypeOf(obj) answers “which object comes next?”, and proto.isPrototypeOf(obj) answers “is proto
anywhere in the chain?”. Object.setPrototypeOf(obj, proto) relinks an object that already exists:
// Asking about prototypes, and changing one after an object exists.
const hindi = {
greet() {
return `Namaste, ${this.name}`;
},
};
const tamil = {
greet() {
return `Vanakkam, ${this.name}`;
},
};
const guest = Object.create(hindi);
guest.name = 'Kavya';
console.log(Object.getPrototypeOf(guest) === hindi); // the next object of the chain
// isPrototypeOf looks along the whole chain. Calling it through Object.prototype works for every object, even one
// that does not inherit it, which is why linters ask for this form.
const inChainOf = (proto, obj) => Object.prototype.isPrototypeOf.call(proto, obj);
console.log(inChainOf(hindi, guest), inChainOf(Object.prototype, guest), inChainOf(tamil, guest));
console.log(guest.greet());
Object.setPrototypeOf(guest, tamil); // works, but engines optimise objects whose prototype never changes
console.log(guest.greet());
// Three changes the language refuses with a TypeError.
const attempts = {
'a cycle': () => Object.setPrototypeOf(tamil, guest), // guest already inherits from tamil
'Object.prototype': () => Object.setPrototypeOf(Object.prototype, tamil), // its prototype is fixed at null
'a frozen object': () => Object.setPrototypeOf(Object.freeze({}), tamil), // non-extensible objects keep theirs
};
for (const [label, attempt] of Object.entries(attempts)) {
try {
attempt();
console.log(label, '-> changed');
} catch (err) {
console.log(label, '->', err.name);
}
}
// __proto__ is an accessor that objects inherit from Object.prototype; an object without that prototype lacks it.
console.log(guest.__proto__ === tamil); // works, but prefer Object.getPrototypeOf
const bare = Object.create(null);
bare.__proto__ = tamil; // no accessor here: this creates an ordinary property called "__proto__"
console.log(Object.getPrototypeOf(bare), Object.keys(bare)); Output
true true true false Namaste, Kavya Vanakkam, Kavya a cycle -> TypeError Object.prototype -> TypeError a frozen object -> TypeError true null [ '__proto__' ]
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node inspect-and-change.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
After Object.setPrototypeOf(guest, tamil), the same object greets with Vanakkam instead of Namaste. The language
refuses three kinds of change with a TypeError:
- A cycle. Linking
tamiltoguest, which already inherits fromtamil, would make a loop that a lookup could never leave. OrdinarySetPrototypeOf checks the chain first and returnsfalse, andObject.setPrototypeOfturns that into the error. Object.prototypeitself. The specification makes it an immutable prototype exotic object: its prototype isnulland staysnull.- A non-extensible object, such as a frozen one, keeps the prototype it has.
obj.__proto__ also reads and writes the prototype, but it is an accessor property that objects inherit from
Object.prototype. The 2026 edition of the specification keeps it only as a legacy feature marked normative
optional: web browsers are generally required to provide it, other engines may leave it out, and the specification
asks programmers not to rely on legacy features in new code. An object without Object.prototype in its chain has no
such accessor: bare.__proto__ = tamil created an ordinary property named __proto__, and the prototype stayed
null. Prefer Object.getPrototypeOf and Object.setPrototypeOf, which work for every object.
Set the prototype when you create the object
MDN warns that changing an object’s prototype is “a very slow operation in every browser and JavaScript engine”, because engines optimise property access on the assumption that prototypes stay put, and that the effects can reach any code that uses the changed object. This run makes 300,000 objects with the same prototype three ways:
// How long V8 takes to make 300,000 objects that share one prototype, when the prototype is given at creation and
// when it is changed afterwards. Each way runs five rounds, taking turns; the fastest round of each is printed.
const proto = {
describe() {
return `item ${this.id}`;
},
};
const ways = {
'Object.create(proto), then id': (id) => {
const item = Object.create(proto);
item.id = id;
return item;
},
'{ __proto__: proto, id }': (id) => ({ __proto__: proto, id }),
'{ id }, then Object.setPrototypeOf': (id) => Object.setPrototypeOf({ id }, proto),
};
const fastest = Object.fromEntries(Object.keys(ways).map((name) => [name, Infinity]));
let last;
for (let round = 0; round < 5; round++) {
for (const [name, make] of Object.entries(ways)) {
const items = new Array(300_000); // the objects are kept, so V8 cannot skip making them
const start = performance.now();
for (let i = 0; i < items.length; i++) items[i] = make(i);
fastest[name] = Math.min(fastest[name], performance.now() - start);
last = items.at(-1);
}
}
for (const [name, ms] of Object.entries(fastest)) console.log(`${name.padEnd(36)} ${ms.toFixed(1).padStart(6)} ms`);
console.log('every way gives the same objects:', last.describe(), Object.getPrototypeOf(last) === proto); Output
Object.create(proto), then id 19.1 ms
{ __proto__: proto, id } 132.7 ms
{ id }, then Object.setPrototypeOf 66.9 ms
every way gives the same objects: item 299999 true
This output changes from run to run: the times depend on the computer and on what else it is running
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node prototype-cost.mjs
In the recorded run on Node.js 24.21.0, Object.create was the fastest by a wide margin. Relinking each object
after it was made took several times as long, and in V8 the __proto__ key in a literal was slow as well; which of
those two comes last varies from run to run. The numbers on your computer will differ, but the advice does not:
decide the prototype when the object is made, with Object.create, a class or a constructor, and leave it alone
afterwards.
Leave the built-in prototypes alone
Because every array is linked to Array.prototype, a method added there appears on every array of the program,
including the arrays of every library on the page. That reach is exactly the problem:
// Two ways a change to a built-in prototype goes wrong, shown with methods that exist today. Every change is
// undone at once, but in a real program it would affect all the code in the page or process.
// 1. A helper library replaces a standard method with its own version. Now every other piece of code gets the
// library's behaviour: this old-style includes() uses indexOf, which never finds NaN.
const standardIncludes = Array.prototype.includes;
Array.prototype.includes = function (value) {
return this.indexOf(value) !== -1;
};
console.log('with the patch: [NaN].includes(NaN) is', [NaN].includes(NaN));
Array.prototype.includes = standardIncludes;
console.log('standard: [NaN].includes(NaN) is', [NaN].includes(NaN));
// 2. A library adds a method only when the engine has none of that name. Its at() was written before the standard
// one and ignores negative indexes, so callers wrote code that expects undefined for at(-1).
if (!Array.prototype.at) {
Array.prototype.at = function (index) {
return this[index];
};
}
const fares = [20, 30, 40];
console.log('library code expects undefined, the engine now answers', fares.at(-1));
// The safe version of such a helper is a plain function: it changes nothing that other code relies on.
const lastOf = (list) => list[list.length - 1];
console.log('lastOf(fares) is', lastOf(fares), '| Array.prototype.includes restored:', Array.prototype.includes === standardIncludes); Output
with the patch: [NaN].includes(NaN) is false standard: [NaN].includes(NaN) is true library code expects undefined, the engine now answers 40 lastOf(fares) is 40 | Array.prototype.includes restored: true
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node builtin-patches.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
- A library that replaces a standard method changes it for everyone. Here an old-style
includesbuilt onindexOfmade[NaN].includes(NaN)returnfalse, which breaks code that relies on the standard method findingNaN. - A library that adds a method only when it is missing is caught out once the standard ships a method of the same
name with other rules. The engine’s
at(-1)returned 40, the last fare, where code written for the library’s version expectedundefined.
This has shaped the language itself. Firefox’s first attempt to ship Array.prototype.flatten broke a well-known site
built on MooTools, an older library with its own flatten, and the committee that maintains JavaScript, TC39, renamed
the method flat. Grouping methods planned for Array.prototype ran into the Sugar library and into code that used
arrays as maps, and became the static Object.groupBy and Map.groupBy instead. A method added by plain assignment
is also enumerable, so it turns up in every for…in loop over an array, as the lesson on for…of and for…in
showed. Write helpers as plain functions or in your own modules instead, such as lastOf(list) in the example, and let
ESLint’s no-extend-native rule report new code that patches a built-in.
Exercise: who owns this property?
Exercise · Easy · JavaScript
Find which object in the chain owns a property
Write getPropertyOwner(obj, key). It returns the object in the prototype chain of obj that has key as an own property: obj itself, one of its prototypes, or Object.prototype. When no object in the chain has the key, it returns null.
- Start at
objand move up one prototype at a time, the way a property lookup does. The first object that owns the key is the answer, so a property that hides one of the same name further up wins. - A property counts when the object has it, whatever its value: a property that holds
undefinedstill has an owner. - Do not read the property's value. A getter must not run just because you looked for its owner.
- It must work for keys that are symbols, for properties that are not enumerable, such as an array's
length, and for objects with no prototype at all (Object.create(null)).
const base = { wheels: 4 };
const bike = Object.create(base);
bike.wheels = 2;
getPropertyOwner(bike, 'wheels') === bike; // true: the nearest owner
getPropertyOwner(Object.create(base), 'wheels') === base; // true
getPropertyOwner(bike, 'toString') === Object.prototype; // true
getPropertyOwner(bike, 'colour'); // null
The starter code only checks whether the key can be reached at all, and then names the wrong object. Run the sample tests to see where it goes wrong.
Starter code · owner.mjs
// getPropertyOwner(obj, key): the object in the prototype chain of obj that owns key, or null.
// This version only checks whether the key can be reached, so it always names obj itself.
export function getPropertyOwner(obj, key) {
if (key in obj) {
return obj;
}
return null;
} The sample tests · owner.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { getPropertyOwner } from './owner.mjs';
test('an own property belongs to the object itself, even when it holds undefined', () => {
const order = { id: 7, coupon: undefined };
assert.equal(getPropertyOwner(order, 'id'), order);
assert.equal(getPropertyOwner(order, 'coupon'), order);
});
test('an inherited property belongs to the prototype that holds it', () => {
const vehicle = { wheels: 4 };
const twoWheeler = Object.create(vehicle);
const scooter = Object.create(twoWheeler);
assert.equal(getPropertyOwner(scooter, 'wheels'), vehicle);
});
test('the nearest owner wins when a name is hidden further down the chain', () => {
const vehicle = { wheels: 4 };
const twoWheeler = Object.create(vehicle);
twoWheeler.wheels = 2;
const scooter = Object.create(twoWheeler);
assert.equal(getPropertyOwner(scooter, 'wheels'), twoWheeler);
});
test('built-in methods belong to the built-in prototypes; an array owns its length', () => {
const marks = [62, 75];
assert.equal(getPropertyOwner({}, 'toString'), Object.prototype);
assert.equal(getPropertyOwner(marks, 'push'), Array.prototype);
assert.equal(getPropertyOwner(marks, 'length'), marks);
});
test('a key that no object in the chain has gives null', () => {
assert.equal(getPropertyOwner({ a: 1 }, 'b'), null);
assert.equal(getPropertyOwner(Object.create({ a: 1 }), 'b'), null);
}); A hint
Use a loop: for (let current = obj; current !== null; current = Object.getPrototypeOf(current)) { … }. Inside it, ask Object.hasOwn(current, key), which looks only at current itself, never runs a getter and works on objects without a prototype. Return current as soon as it says true, and null after the loop.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Self-check
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · What is the prototype chain in JavaScript?
Every object has an internal link, [[Prototype]], to another object or to null. When you read a property,
JavaScript looks at the object’s own properties first; if the name is not there, it follows the link to the
prototype, then to the prototype’s prototype, until it finds the name or reaches null, which gives undefined. That
series of objects is the prototype chain, and it is how objects share methods: an array finds push on
Array.prototype and toString on Object.prototype. Writing is different: an assignment creates or changes an own
property, which hides one of the same name further up, unless the chain holds a setter or a read-only property for
that name. Object.getPrototypeOf reads the link, Object.create sets it when an object is made, and classes and
constructor functions set it for every object they create.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A settings module creates each user's settings with Object.create(DEFAULTS) and then assigns only what the user changed. The web page shows every value correctly, but the mobile app, which reads the saved JSON, shows blank themes and font sizes for most users. Explain why, then fix both saving and loading.
-
Mid to senior
A shared utilities file adds Array.prototype.unique = function () { … } so that teams can write tags.unique(). In a code review you ask for it to be removed, and the author replies that it has worked for two years. What can it break today and in future releases, and how would you migrate away from it?
Key takeaways
- Every object has a
[[Prototype]]: another object ornull.Object.getPrototypeOfreads it, and nearly every chain ends atObject.prototypeand thennull. - A read checks the object, then each prototype in turn; the first owner wins, and a name found nowhere gives
undefined. - A write creates or changes an own property and shadows the inherited one, except when the chain holds a setter, which runs, or a read-only property, which refuses the write. Arrays and objects on a prototype are shared.
Object.create(proto)sets the prototype when an object is made;Object.create(null)or{ __proto__: null }makes a table with no inherited keys, at the price of having notoStringand nohasOwnProperty.- Changing a prototype afterwards is slow and can surprise other code; cycles,
Object.prototypeand non-extensible objects are refused. PreferObject.getPrototypeOfandObject.setPrototypeOfto__proto__. - Never add or replace methods on built-in prototypes in shared code: it changes every object of the program and can clash with methods the standard adds later.
References
- ECMAScript 2026 Language Specification: ordinary object internal methods and internal slots (Ecma International)
- ECMAScript 2026 Language Specification: OrdinaryGet (Ecma International)
- ECMAScript 2026 Language Specification: OrdinarySetWithOwnDescriptor (Ecma International)
- ECMAScript 2026 Language Specification: OrdinarySetPrototypeOf (Ecma International)
- ECMAScript 2026 Language Specification: immutable prototype exotic objects (Ecma International)
- ECMAScript 2026 Language Specification: Object.create (Ecma International)
- ECMAScript 2026 Language Specification: Object.prototype.__proto__ (Ecma International)
- ECMAScript 2026 Language Specification: conformance (normative optional and legacy clauses) (Ecma International)
- Inheritance and the prototype chain (MDN Web Docs)
- Object.setPrototypeOf() (MDN Web Docs)
- Object.prototype.__proto__ (MDN Web Docs)
- #SmooshGate FAQ (Chrome for Developers)
- Proposal: Array grouping (why static methods) (Ecma TC39)
- ESLint rule: no-extend-native (OpenJS Foundation (ESLint))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress