Module 9 ¡ this, Prototypes and Classes
this and Prototypes, the Picture: Decided at the Call, Found up the Chain
In this lesson
- Predict what
thisis 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
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
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
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
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' ]
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
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.
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
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.
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.
Where this is used
- Node.js: every array you make links to
Array.prototype, which is why[1, 2].mapexists 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
reqandresobjects of a route link to prototypes that Express fills with helpers likeres.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.
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.
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
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
prototypeproperty I have seen on functions?Not quite, and the names really are confusing. Every object has a prototype, the hidden link that
Object.getPrototypeOfreturns. A function also has an ordinary property namedprototype, which becomes the prototype of objects made withnew. 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.prototypethat reads or sets an object's prototype. It still works, butObject.getPrototypeOfandObject.createsay 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
TypeErrorinstead. Every chain ends atnull, so a lookup always stops.
Key takeaways
thisis 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
nullafterObject.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
thisto 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