Skip to content
aviral gupta

// I2.1 · ~33 min · Intermediate

The prototype chain

After this lesson you can say where JavaScript finds a property: on the object itself or further up its prototype chain. You build objects on a shared prototype with Object.create.

Lesson 1 of 5 in I2 Objects, prototypes and classes

Start of the module

You will be able to

  • Explain how a property lookup walks the prototype chain, and follow the chain with Object.getPrototypeOf
  • Build an object on a chosen prototype with Object.create, and tell own from inherited properties with Object.hasOwn
  • Share methods on a prototype, and shadow an inherited property with an own one
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B5.3. What does this print?

    const base = {a: 1, b: 2};
    const copy = {...base, b: 3};
    console.log(copy.a, copy.b, base.b);
  2. Predict · Activity 2 of 7

    Predict before you read on. obj has no keys of its own. What does this print?

    const proto = {greet() { return "hello"; }};
    const obj = Object.create(proto);
    console.log(Object.keys(obj).length, obj.greet());
  3. Practice · Activity 3 of 7

    Fill in the method that makes a new object whose prototype is animal.

    const animal = {speak() { return "..."; }};
    const cat = Object.____(animal);
    const cat = Object.(animal);
  4. Practice · Activity 4 of 7

    Match each term or call to what it means.

  5. Practice · Activity 5 of 7

    Two objects share one prototype, and a sets color. What does this print?

    const proto = {color: "red"};
    const a = Object.create(proto);
    const b = Object.create(proto);
    a.color = "blue";
    console.log(a.color, b.color, proto.color);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. for...in and Object.keys on the same object. What does this print?

    const proto = {shared: 1};
    const obj = Object.create(proto);
    obj.own = 2;
    const seen = [];
    for (const key in obj) seen.push(key);
    console.log(seen.join(","), Object.keys(obj).join(","));
  7. Apply · Activity 7 of 7

    Mini-task: write a prototype accountProto with two methods, deposit(amount), which adds to this.balance and returns it, and describe(), which returns a text such as "Ada: 50 EUR". Write makeAccount(owner), which builds an account on accountProto with Object.create. Make two accounts, deposit into one, and show that both share one deposit method.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Two animals, one prototype

animal holds a default sound and a speak() method. cat and fish are made with Object.create(animal), so both link to it. cat gets its own sound, which shadows the default; fish keeps the inherited one. The program then checks the links and the kinds of property, and follows the chain to its end.

main.js

// A prototype with a shared method.
const animal = {
  sound: "...",
  speak() {
    return `${this.name} says ${this.sound}`;
  }
};

// Two objects that link to it.
const cat = Object.create(animal);
cat.name = "Mia";
cat.sound = "meow"; // an own property: it shadows animal.sound
const fish = Object.create(animal);
fish.name = "Bubbles";

console.log(cat.speak());
console.log(fish.speak());
console.log(Object.getPrototypeOf(cat) === animal);
console.log(Object.hasOwn(cat, "sound"), Object.hasOwn(fish, "sound"));
console.log("speak" in fish, Object.hasOwn(fish, "speak"));
console.log(cat.speak === fish.speak);
console.log(Object.keys(cat));

// The chain goes on: animal links to Object.prototype, which links to null.
console.log(Object.getPrototypeOf(animal) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));

Run it with

node main.js

Output

Mia says meow
Bubbles says ...
true
true false
true false
true
[ 'name', 'sound' ]
true
null
  • speak() exists once, on animal, but this is cat or fish, whichever it was called on.
  • fish has no own sound, so the lookup found animal’s "...".
  • "speak" in fish is true, Object.hasOwn(fish, "speak") is false: speak is inherited.
  • Object.keys(cat) lists only cat’s own properties.
  • The chain ends at null, the prototype of Object.prototype.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

Own, inherited or missing

Write describe(obj, key). It returns "own" when key is an own property of obj, "inherited" when obj only gets key from its prototype chain, and "missing" when the key is not found at all. The starter uses only the in operator, so it calls inherited properties own.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: inherited keys come back as own. Which check is true only for the object itself?

  2. Hint 2

    Object.hasOwn(obj, key) is true for own properties only; key in obj is true for own and inherited ones.

  3. Hint 3

    Check Object.hasOwn first and return "own", then key in obj and return "inherited", else "missing".

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function describe(obj, key) {
  if (Object.hasOwn(obj, key)) return "own";
  if (key in obj) return "inherited";
  return "missing";
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function describe(obj, key) {
  if (key in obj) return "own";
  return "missing";
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {describe} from './main.js';

test('A property on the object itself is own', () => {
  const got = describe({name: 'Ada'}, 'name');
  assert.equal(got, 'own', `describe({name}, 'name') gave ${got}`);
});

test('A property from the prototype is inherited', () => {
  const child = Object.create({kind: 'fruit'});
  const got = describe(child, 'kind');
  assert.equal(got, 'inherited', `describe(child, 'kind') gave ${got}`);
});

test('toString on a plain object is inherited', () => {
  const got = describe({}, 'toString');
  assert.equal(got, 'inherited', `describe({}, 'toString') gave ${got}`);
});

test('An unknown key is missing', () => {
  const got = describe({}, 'colour');
  assert.equal(got, 'missing', `describe({}, 'colour') gave ${got}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Counters that share one method

makeCounter(start) should return a counter whose increment() adds one to its count and returns it. The method must live once, on counterProto, and not on every counter. The starter gives each counter its own copy of increment. Build the counter with Object.create(counterProto) and give it only an own count.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: increment works, but it is an own property of every counter. Where should it live instead?

  2. Hint 2

    Object.create(counterProto) gives an empty object that inherits increment.

  3. Hint 3

    const counter = Object.create(counterProto); counter.count = start; return counter;

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export const counterProto = {
  increment() {
    this.count += 1;
    return this.count;
  }
};

export function makeCounter(start) {
  const counter = Object.create(counterProto);
  counter.count = start;
  return counter;
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export const counterProto = {
  increment() {
    this.count += 1;
    return this.count;
  }
};

export function makeCounter(start) {
  return {
    count: start,
    increment() {
      this.count += 1;
      return this.count;
    }
  };
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {counterProto, makeCounter} from './main.js';

test('increment adds one and returns the new count', () => {
  const counter = makeCounter(5);
  counter.increment();
  const got = counter.increment();
  assert.equal(got, 7, `two increments from 5 gave ${got}`);
});

test('The prototype of a counter is counterProto', () => {
  const counter = makeCounter(0);
  assert.equal(Object.getPrototypeOf(counter) === counterProto, true, 'Object.getPrototypeOf(counter) should be counterProto');
});

test('increment is inherited, not an own property', () => {
  const counter = makeCounter(0);
  assert.equal(Object.hasOwn(counter, 'increment'), false, 'each counter has its own increment; it should come from counterProto');
});

test('Two counters keep their own counts', () => {
  const a = makeCounter(0);
  const b = makeCounter(10);
  a.increment();
  assert.deepEqual([a.count, b.count], [1, 10], `the counts are ${a.count} and ${b.count}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Calling Object.create without a prototype

const animal = {speak() { return "..."; }};
const cat = Object.create();
console.log(cat.speak());

What Node.js prints

TypeError: Object prototype may only be an Object or null: undefined

Why, and the fix

Object.create needs the object to link to as its first argument; it does not copy or guess one. Pass the prototype: Object.create(animal). When you really want an object without any prototype, say so with Object.create(null).

Calling hasOwnProperty on an object without a prototype

const stock = Object.create(null);
stock.apples = 3;
console.log(stock.hasOwnProperty("apples"));

What Node.js prints

TypeError: stock.hasOwnProperty is not a function

Why, and the fix

hasOwnProperty is inherited from Object.prototype. An object made with Object.create(null) has no prototype, so it has no hasOwnProperty either. Use the static method instead, which works for every object: Object.hasOwn(stock, "apples").

An arrow function as a shared method

const proto = {
  hello: () => `I am ${this.name}`
};
const a = Object.create(proto);
a.name = "Ada";
console.log(a.hello());

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'name')

Why, and the fix

An arrow function has no this of its own; it takes this from the code around it. At the top of a module that is undefined, so this.name fails, whichever object called hello. Write shared methods with the method syntax, hello() { return `I am ${this.name}`; }, so this is the object before the dot.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

An object links to its prototype

Every object has a hidden link to another object, its prototype. When you read obj.x, JavaScript looks for x on obj first. If obj has no x, it looks on obj’s prototype, then on that object’s prototype, and so on. The first x it finds wins. At the end of the chain is null; if x was not found by then, the result is undefined. Object.getPrototypeOf(obj) gives you the next link. A plain object {} links to Object.prototype, which is why every object has toString.

Own and inherited properties

Object.create(proto) makes a new, empty object whose prototype is proto. Nothing is copied: a property added to proto later is visible through the new object too. A property that sits on the object itself is an own property; one found further up is inherited. Object.hasOwn(obj, key) is true only for own properties, while key in obj is true for both. Object.keys, JSON.stringify and spread show only own properties. Object.create(null) makes an object with no prototype at all, so it has no toString and no hasOwnProperty.

Shared methods and shadowing

Put a method on the prototype and every object that links to it can call it, yet the function exists only once. Inside the method, this is the object it was called on, not the prototype, so one speak() can print a different name for each animal. Writing obj.x = value always creates or changes an own property on obj; it never changes the prototype. That own property now hides, or shadows, the inherited one for this object only. delete obj.x removes the own property, and the inherited value shows again.

Sources

Last reviewed October 5, 2026