Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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 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 seven values JavaScript · builtin-chains.mjs
// 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

  • An object literal is linked to Object.prototype, whose own prototype is null. That is where nearly every chain ends, which is why almost every object has toString and hasOwnProperty.
  • Arrays, Maps, functions and Dates have one more step: their own built-in prototype, which holds push, get, call or getFullYear, linked in turn to Object.prototype.
  • Object.create(null) makes an object whose prototype is null from 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 temporary String object, whose prototype is String.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:

A three-level chain, walked level by level JavaScript · chain-walk.mjs
// 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

A prototype chain: scooter links to twoWheeler, then vehicle, then Object.prototype, then null; each box lists the properties it owns.scooterowns: nametwoWheelerowns: wheels = 2vehicleowns: wheels = 4, describe()Object.prototypeowns: toString(), hasOwnProperty() …null(the chain ends)[[Prototype]][[Prototype]][[Prototype]][[Prototype]]

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.wheels is found on twoWheeler, so the 4 on vehicle is never reached. A property lower in the chain shadows one of the same name higher up.
  • scooter.describe() is found on vehicle, two links up. Inside it, this is still scooter, the object the method was called on, so one describe serves every object that inherits it. That is the rule the lesson on this described: the call decides, not the place where the method lives.
  • scooter.colour is found nowhere. Once the lookup reaches null the answer is undefined, quietly. A missing variable throws a ReferenceError, 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:

What an assignment changes, and the two exceptions JavaScript · reads-and-writes.mjs
// 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

  1. asha.theme = 'dark' created an own property on asha. ravi and defaults still say light.
  2. delete asha.theme removed only asha’s own property, so the inherited theme showed through again. delete never reaches into a prototype.
  3. The common mistake: asha.tags.push('vip') is not an assignment to asha at all. It reads asha.tags, finds the one array that lives on defaults, and changes that array, so ravi sees 'vip' too. Arrays and objects stored on a prototype are shared by everything that inherits them.
  4. Assigning asha.tags = ['new'] gave asha an 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 command table with and without a prototype JavaScript · dictionary.mjs
// 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

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:

Reading, testing and changing a prototype JavaScript · inspect-and-change.mjs
// 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

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 tamil to guest, which already inherits from tamil, would make a loop that a lookup could never leave. OrdinarySetPrototypeOf checks the chain first and returns false, and Object.setPrototypeOf turns that into the error.
  • Object.prototype itself. The specification makes it an immutable prototype exotic object: its prototype is null and stays null.
  • 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:

Prototype at creation or afterwards: 300,000 objects, timed JavaScript · prototype-cost.mjs
// 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 patched built-in goes wrong JavaScript · builtin-patches.mjs
// 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

  • A library that replaces a standard method changes it for everyone. Here an old-style includes built on indexOf made [NaN].includes(NaN) return false, which breaks code that relies on the standard method finding NaN.
  • 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 expected undefined.

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.

JavaScript Linter Paste builtin-patches.mjs and choose the Strict rule set: no-extend-native marks all three lines that assign to Array.prototype. Paste inspect-and-change.mjs to see no-proto mark both uses of __proto__. JavaScript & TypeScript Online Compiler Run chain-walk.mjs, then give twoWheeler a describe() of its own and predict which one scooter.describe() calls.

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 obj and 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 undefined still 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.

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.

  1. Question 1 of 5 What does this program print?

    Read the code, then choose one answer.

    const base = { size: 'M' };
    const shirt = Object.create(base);
    shirt.size = 'L';
    delete shirt.size;
    console.log(shirt.size);
    Show the answer to question 1

    Answer: M

    The assignment gave shirt an own size, which hid the inherited one. delete removed only that own property, so the next read walks up to base again and finds 'M'. delete never changes a prototype.

  2. Question 2 of 5 What does this program print?

    Read the code, then choose one answer.

    const proto = { items: [] };
    const a = Object.create(proto);
    const b = Object.create(proto);
    a.items.push('tea');
    console.log(b.items.length, Object.hasOwn(a, 'items'));
    Show the answer to question 2

    Answer: 1 false

    a.items is a read: it finds the one array on proto, and push changes that array. Nothing was assigned to a, so it still has no own items, and b sees the same array with one element in it.

  3. Question 3 of 5 Which of these Object.setPrototypeOf calls throw a TypeError? Choose every one that does.

    Choose every answer that is right.

    Show the answer to question 3

    Answer:

    • Giving Object.prototype a new prototype
    • Giving parent the prototype child, when child already inherits from parent
    • Giving a frozen object a new prototype

    The first would make a cycle, which the language checks for. Object.prototype is an immutable prototype exotic object, so its prototype stays null. A frozen object is not extensible and keeps its prototype. An object with a null prototype is an ordinary, extensible object, so giving it a prototype works.

  4. Question 4 of 5 const bare = Object.create(null); bare.__proto__ = settings; leaves the prototype of bare at null. Why?

    Choose one answer.

    Show the answer to question 4

    Answer: __proto__ is an accessor inherited from Object.prototype, which bare does not have, so the assignment creates an ordinary property

    obj.__proto__ = value works only because most objects inherit the __proto__ accessor from Object.prototype. bare inherits nothing, so the write simply creates an own property called __proto__. Object.setPrototypeOf(bare, settings) would have changed the prototype.

  5. Question 5 of 5 Shared code needs a helper that returns the last element of an array. Which way is safe?

    Choose one answer.

    Show the answer to question 5

    Answer: An exported function, lastOf(list), in one of your own modules

    Every option that touches Array.prototype changes every array of the program. A non-enumerable property no longer shows up in for…in loops, but it can still clash with a method the standard adds later, and the conditional version silently stops being used once the engine has a method of that name. A plain function changes nothing that other code relies on.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Every object has a [[Prototype]]: another object or null. Object.getPrototypeOf reads it, and nearly every chain ends at Object.prototype and then null.
  • 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 no toString and no hasOwnProperty.
  • Changing a prototype afterwards is slow and can surprise other code; cycles, Object.prototype and non-extensible objects are refused. Prefer Object.getPrototypeOf and Object.setPrototypeOf to __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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.