Learn JavaScript

Lesson 1 of 9 ¡ this, Prototypes and Classes

Module 9 ¡ this, Prototypes and Classes

this and Prototypes, the Picture: Decided at the Call, Found up the Chain

FreeReading

In this lesson

  • Predict what this is in a call by looking at how the function is called, not where it was written.
  • Explain how a property lookup climbs from an object to its prototype and on to Object.prototype.
  • Build a small chain with Object.create, and share one method between many objects through it.

Mitu writes a quiz app. Each player is an object with a name and a method that greets the player. It works when she calls player.hello(). Then she stores the method in a variable to call it later, and the greeting comes out as "Hi, undefined". The method did not change. Only the way she called it changed.

That one surprise is half of this module. The other half is a question you met in Module 8: how does {} have a toString that nobody wrote? Both answers are short. this is decided when a function is called. A missing property is looked for up a chain of objects called prototypes.

A method that forgets its object

A method is a function stored as a property of an object. Inside it, the word this lets the method reach the object it belongs to, so it can read this.name. Here is Mitu's player, and her two calls.

const player = {
  name: "Rafi",
  hello() {
    return "Hi, " + this.name;
  },
};
console.log(player.hello());
const hello = player.hello;
console.log(hello());
console.log(hello === player.hello);
Hi, Rafi
Hi, undefined
true
Run in Compiler

The last line proves it is the same function. Yet the second call lost the name. When the function was called as hello(), nothing was in front of the dot, so this was not player. In this file this became the global object, which has no name. Lesson 02 shows that strict code gives undefined instead, and then the call crashes.

this is decided at the call

Here is the rule in one line. this is the object in front of the dot at the moment of the call. Where the function was written does not matter. Who holds it does not matter either.

function describe() {
  return this.name + " has " + this.score + " points";
}
const rafi = { name: "Rafi", score: 7, describe };
const nadia = { name: "Nadia", score: 9, describe };
console.log(rafi.describe());
console.log(nadia.describe());
console.log(rafi.describe === nadia.describe);
Rafi has 7 points
Nadia has 9 points
true
Run in Compiler

One function, two objects. rafi.describe() runs it with this as rafi, and nadia.describe() runs the very same function with this as nadia. So this is like a parameter that you pass by where you put the dot. Lesson 02 lists every way a call can set it.

Every object has a prototype

Now the second question. In Module 8 you saw that "toString" in {} is true, but Object.hasOwn({}, "toString") is false. The object does not own toString. It reaches it through its prototype: a hidden link to another object.

const empty = {};
const proto = Object.getPrototypeOf(empty);
console.log(proto === Object.prototype);
console.log(Object.hasOwn(empty, "toString"));
console.log(Object.hasOwn(proto, "toString"));
console.log(Object.getPrototypeOf(proto));
true
false
true
null
Run in Compiler

Object.getPrototypeOf(o) returns the object that o links to. For a plain object, that is Object.prototype, which owns toString and a few other methods. Object.prototype links to nothing: its prototype is null. That is where every chain ends.

A lookup climbs the chain

When you read o.key, JavaScript first checks whether o owns key. If not, it checks the prototype of o, then that object's prototype, and so on. It stops at the first object that owns the key. If it reaches null, the answer is undefined. That path of links is the prototype chain.

You can build a chain yourself with Object.create(p), which makes a new empty object whose prototype is p.

const game = { theme: "light", sound: "on" };
const team = Object.create(game);
team.theme = "dark";
const player = Object.create(team);
player.name = "Rafi";
console.log(player.name, player.theme, player.sound);
console.log(Object.hasOwn(player, "theme"));
console.log(player.speed);
Rafi dark on
false
undefined
Run in Compiler

player.name is found on player itself. player.theme is not on player, so the lookup climbs to team and finds "dark" there. It never reaches the game's "light", because team's value shadows it. player.sound climbs two levels. player.speed climbs to the end and gives undefined.

Writing never climbs

Reading climbs the chain. Writing does not. Assigning o.key = v puts the key on o itself, even when a prototype already has that key.

const defaults = { volume: 5 };
const settings = Object.create(defaults);
console.log(settings.volume);
settings.volume = 9;
console.log(settings.volume, defaults.volume);
console.log(Object.keys(settings));
5
9 5
[ 'volume' ]
Run in Compiler

After the write, settings owns its own volume, which shadows the one on defaults. defaults is untouched, so every other object that links to it still reads 5. This is what makes a prototype safe to share. (A setter on the chain is the one exception, and Lesson 02 shows it.)

Methods live on the prototype

Put the two halves together. Many objects can link to one prototype that holds the methods. Each object keeps only its own data. When a method runs, this is the object in front of the dot, so one shared method works for every object.

const playerMethods = {
  hello() {
    return "Hi, " + this.name;
  },
  addPoint() {
    this.score += 1;
    return this.score;
  },
};
const rafi = Object.create(playerMethods);
rafi.name = "Rafi";
rafi.score = 0;
const lima = Object.create(playerMethods);
lima.name = "Lima";
lima.score = 4;
console.log(rafi.hello(), lima.hello());
console.log(rafi.addPoint(), lima.addPoint());
console.log(rafi.hello === lima.hello);
Hi, Rafi Hi, Lima
1 5
true
Run in Compiler

rafi.hello is not on rafi. The lookup climbs to playerMethods and finds it there. Then the call rafi.hello() runs it with this as rafi. That is the whole trick behind classes, which Lesson 02 builds on: a class is a neat way to write exactly this shared prototype.

Watch a lookup climb, one object at a time. The chain is drawn from the object on the left to Object.prototype on the right. The box for Object.prototype shows only three of its many names.

The same steps as a table, one row per step.

StepLookupAtFoundWhat happened
1name0truerafi owns name, so rafi.name stops here and gives a string.
2hello0falserafi does not own hello, so the lookup climbs to playerMethods.
3hello1trueplayerMethods owns hello, so rafi.hello stops here and gives a function.
4toString0falserafi does not own toString, so the lookup climbs to playerMethods.
5toString1falseplayerMethods does not own toString, so the lookup climbs to Object.prototype.
6toString2trueObject.prototype owns toString, so rafi.toString stops here and gives a function.
7speed0falserafi does not own speed, so the lookup climbs to playerMethods.
8speed1falseplayerMethods does not own speed, so the lookup climbs to Object.prototype.
9speed2falseNot even Object.prototype owns speed, and its prototype is null, so rafi.speed is undefined.

Each lookup starts again at rafi, on the left. name stops at once. hello climbs one link. toString climbs two. speed climbs to the end and finds nothing, which is why a missing property is undefined and not an error.

this and the chain at a glance

obj.method()                   // inside method, this is obj: the object before the dot
const f = obj.method; f()      // same function, nothing before the dot: this is lost
Object.getPrototypeOf(o)       // the object o links to, or null at the end
Object.create(p)               // a new empty object whose prototype is p
o.key                          // read: o, then its prototype, then on up, else undefined
o.key = v                      // write: always on o itself, never up the chain
Object.hasOwn(o, "key")        // true only if o itself owns key
Example 1: one method, many tickets

Nadia's cinema prints tickets. The input is a list of buyers and seat numbers. Every ticket object links to one prototype that holds the label method, so the method exists once, however many tickets there are.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
const ticketMethods = {
  label() {
    return this.buyer + " in seat " + this.seat;
  },
};
const tickets = [];
for (let i = 0; i < tokens.length; i += 2) {
  const t = Object.create(ticketMethods);
  t.buyer = tokens[i];
  t.seat = Number(tokens[i + 1]);
  tickets.push(t);
}
for (const t of tickets) console.log(t.label());
console.log(tickets.every((t) => Object.getPrototypeOf(t) === ticketMethods));
console.log(Object.keys(tickets[0]));
Nadia in seat 12
Kenji in seat 7
Sumon in seat 3
true
[ 'buyer', 'seat' ]

Object.keys lists only the ticket's own keys, buyer and seat. label is not copied into any ticket. Each call t.label() climbs to the prototype, and this is the ticket in front of the dot.

Run in Compiler
Example 2: shadowing a default, then removing the shadow

A settings object links to the defaults. Writing a key shadows the default. Deleting the own key brings the default back, because the lookup climbs again.

const defaults = { lang: "en", fontSize: 14 };
const mine = Object.create(defaults);
mine.lang = "bn";
console.log(mine.lang, mine.fontSize);
delete mine.lang;
console.log(mine.lang);
delete mine.fontSize;
console.log(mine.fontSize, defaults.fontSize);
bn 14
en
14 14

delete mine.fontSize does nothing to defaults, because delete only removes an own key, and mine never owned fontSize. The read after it still climbs and finds 14.

Run in Compiler

Where this is used

  • Node.js: every array you make links to Array.prototype, which is why [1, 2].map exists without being copied into each array.
  • V8: Chrome's and Node's engine caches where a property lookup ended up the chain, so repeated reads of a prototype method stay fast.
  • Express: the req and res objects of a route link to prototypes that Express fills with helpers like res.json.

Common mistakes

1. Taking a method out of its object

const counter = {
  count: 0,
  add() {
    this.count += 1;
    return this.count;
  },
};
const add = counter.add;
console.log(add());
console.log(counter.count);
NaN
0

The call add() has nothing in front of the dot, so this is not counter. It adds 1 to a missing count and prints NaN, while counter.count stays 0. Call it as counter.add(), or see the fixes in Lesson 02.

2. Expecting a write to change the prototype

const shared = { color: "red" };
const a = Object.create(shared);
const b = Object.create(shared);
a.color = "blue";
console.log(b.color);
console.log(shared.color);
red
red

Here the hope was to recolor every object. A write lands on a itself, so b and shared still say red. To change what all of them see, write to the prototype: shared.color = "blue".

3. Passing a name to Object.create

const base = { kind: "base" };
const child = Object.create(base);
console.log(child.kind);
const broken = Object.create("base");
console.log(broken.kind);
base
TypeError: Object prototype may only be an Object or null: base

The first call works and prints base. The second stops with TypeError: Object prototype may only be an Object or null: base. Object.create takes the prototype object itself, or null, never the name of a variable as a string.

Brain teaser

Who owns the last word?

Zara builds a chain of three and writes in the middle.

const top = { word: "top" };
const mid = Object.create(top);
const low = Object.create(mid);
mid.word = "mid";
delete low.word;
console.log(low.word, Object.hasOwn(low, "word"));

What does it print? Decide which object owns word after each line.

Exercise 1Easy

Lima builds a quiz game. A player's settings sit on top of the team's settings, which sit on top of the game's defaults. When the game reads a setting, it checks the player first, then the team, then the game. Lima wants a tool that says which level answered each read.

Input. The first line holds n, the number of levels. Each of the next n lines describes one level: its name, a count k, then k pairs of a key and a value. The first level is the top; each next level sits below the one before it. Then comes q, and q keys to read.

Output. One line per key. If some level has the key, print its value, the word from, and the name of the nearest level that has it. Otherwise print missing.

Constraints. 1 <= n <= 10; 0 <= k <= 10 for every level; 1 <= q <= 1000; Level names and keys are 1 to 15 English letters. Values are 1 to 10 letters or digits. Keys in one level are different..

Sample.

3
player 2 theme dark volume 7
team 2 volume 5 language bn
game 3 theme light language en speed normal
5
volume
theme
speed
toString
language
7 from player
dark from player
normal from game
missing
bn from team
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));
Run in Compiler

Graded as chain-lookup, a free problem in this module's problem set.

Exercise 2Medium

Sumon's club keeps member cards. Every card links to one shared prototype with a line method. The method returns the name, then the city, then the number of visits. Print every card's line, then the number of keys each card owns.

Input. n, then n lines, each a member's name, city and visit count.

Output. n lines, each the card's line as name, city and visits, separated by single spaces. Then one more line: owned, a space, and the number of own keys of the first card.

Constraints. 1 <= n <= 1000. Names and cities are 1 to 10 letters. Visits are from 0 to 1000.

Sample. the input below.

3
Rafi Dhaka 12
Mitu Khulna 3
Kenji Sylhet 40
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample output.

Rafi Dhaka 12
Mitu Khulna 3
Kenji Sylhet 40
owned 3
Run in Compiler
Exercise 3Hard

Lima wants to test her picture of shadowing. Her script starts with one object named base, with no keys. Four commands follow. make x y makes x as a new object whose prototype is y. set x k v writes key k with value v on x. del x k deletes x's own key k. get x k prints the value a read of k on x would give, or none.

Input. n, then n commands, one per line.

Output. one line per get.

Constraints. 1 <= n <= 1000. Names, keys and values are 1 to 8 lowercase letters. Every name is made before it is used, and base exists from the start.

Sample. the input below.

7
make team base
make me team
set base theme light
set team theme dark
get me theme
del team theme
get me theme
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Sample output.

dark
light
Run in Compiler

Common doubts

  • Is the prototype the same as the prototype property I have seen on functions?

    Not quite, and the names really are confusing. Every object has a prototype, the hidden link that Object.getPrototypeOf returns. A function also has an ordinary property named prototype, which becomes the prototype of objects made with new. Lesson 02 shows how the two meet.

  • Why not just copy the methods into every object?

    You can, and a factory function from Module 6 does exactly that. It costs one function per object for every method. A shared prototype holds each method once, however many objects use it. Lesson 04 compares the two.

  • What is __proto__?

    It is an old accessor on Object.prototype that reads or sets an object's prototype. It still works, but Object.getPrototypeOf and Object.create say the same thing clearly. Module 8 showed why a key named __proto__ is also a trap.

  • Can a chain go in a circle?

    No. JavaScript refuses to make a prototype link that would close a loop, and throws a TypeError instead. Every chain ends at null, so a lookup always stops.

Key takeaways

  • this is the object in front of the dot when the function is called, not where it was written.
  • Every object links to a prototype, and the chain ends at null after Object.prototype.
  • A read climbs the chain to the first object that owns the key, else gives undefined.
  • A write always lands on the object itself, shadowing any key further up.
  • Methods on a shared prototype exist once and use this to reach each object.
  • Go deeper: Under the Hood, the Chain Walked by Hand, instanceof, and How V8 Sees Class Objects (Pro).

Next: every rule that sets this, and the class syntax that builds the same chains.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every Rule for this, and Every Piece of Class Syntax, One by One