Q1For a summary method written in a class, what does jsCourse.hasOwnProperty("summary") return?
Prototypes — What's Behind a Class
Covers prototype, where class methods live, and the prototype chain: Object.getPrototypeOf, hasOwnProperty vs. in, and adding methods later.
A course created from a class can call summary(), yet the instance itself doesn't have that method. If you don't know where the method lives, you also can't explain why a child class can call its parent class's methods.
This article covers prototype, where class methods go, and the prototype chain, the order in which JavaScript searches for a name.
Calling a Method the Instance Doesn't Have — prototype
If you create 300 courses, you might wonder whether 300 copies of the summary body get created too, one per instance. Object.keys(jsCourse) doesn't list summary, so it can't tell you where the methods you call are stored.
A prototype (another object that an object inherits properties from) can be retrieved with Object.getPrototypeOf(), and for jsCourse it's Course.prototype. Any name the object doesn't have itself is searched for there.
The class's prototype property is that same object, but jsCourse.prototype is undefined.
class Course {
constructor(title, hours) {
this.title = title; // Course name
this.hours = hours; // Study hours
}
summary() { return `${this.title} (${this.hours} hours)`; }
}
const jsCourse = new Course("JS Basics", 12);
const sqlCourse = new Course("SQL Basics", 8);
// summary isn't one of the instance's own keys
console.log(Object.keys(jsCourse).join(", ")); // title, hours
// jsCourse's prototype is Course.prototype (not jsCourse.prototype)
console.log(Object.getPrototypeOf(jsCourse) === Course.prototype); // true
console.log(jsCourse.prototype); // undefined
// Both courses use the single summary on Course.prototype
console.log(jsCourse.summary === sqlCourse.summary); // true
console.log(sqlCourse.summary()); // SQL Basics (8 hours)
There's only one body, but this is set to whatever is left of the dot on the calling line, so sqlCourse.summary() builds its string from sqlCourse's values. Even with 300 courses, the only thing that multiplies is the instances holding title and hours.
Searching in Order Until null — The Prototype Chain
If you create a live-streamed course with class LiveCourse extends Course, liveCourse.summary() works too. But the only method written in liveCourse's prototype, LiveCourse.prototype, is schedule.
Names are searched for along the prototype chain (the sequence of references you get by following the prototype's prototype until null), starting from the instance. extends makes Course.prototype the next link after LiveCourse.prototype, and after that comes Object.prototype (a built-in object that sits at the end of the chain).
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} hours)`; }
}
class LiveCourse extends Course { schedule() { return "Tuesdays from 8 p.m."; } }
const liveCourse = new LiveCourse("React in Practice", 10);
// Get the prototypes one level at a time
const step1 = Object.getPrototypeOf(liveCourse);
const step2 = Object.getPrototypeOf(step1);
const step3 = Object.getPrototypeOf(step2);
console.log(step1 === LiveCourse.prototype); // true
console.log(step2 === Course.prototype); // true
console.log(step3 === Object.prototype); // true
console.log(Object.getPrototypeOf(step3)); // null (the end of the chain)
// summary is found at the second level, Course.prototype
console.log(liveCourse.summary()); // React in Practice (10 hours)
The chain includes Course.prototype, not Course itself. Methods marked static live on Course, so reading one from an instance searches all the way to null without finding it and gives undefined; calling it throws a TypeError.
A Same-Named Key on the Instance Is Found First
If you assign liveCourse.summary = "Coming soon";, the name is found on liveCourse itself, and the prototype is never searched. liveCourse.summary() then throws TypeError: liveCourse.summary is not a function.
Limiting the Check to the Instance's Own Keys — hasOwnProperty
Suppose you receive some course data and want to check whether summary has been overwritten on individual courses by direct assignment. "summary" in jsCourse is true even for a method on the prototype, so it can't tell you whether the value was assigned to the instance itself.
hasOwnProperty (a method that returns true only when the object itself has the key) takes the key as a string, as in jsCourse.hasOwnProperty("title"). The method itself lives on Object.prototype and is found through the chain.
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} hours)`; }
}
const jsCourse = new Course("JS Basics", 12);
const sqlCourse = new Course("SQL Basics", 8);
sqlCourse.summary = "Coming soon"; // Overwrite it for this course only
// in is true even for names on the prototype
console.log("summary" in jsCourse); // true
console.log("summary" in sqlCourse); // true
// hasOwnProperty is true only for keys the course itself has
console.log(jsCourse.hasOwnProperty("summary")); // false
console.log(sqlCourse.hasOwnProperty("summary")); // true (it's been overwritten)
console.log(Course.prototype.hasOwnProperty("summary")); // true
title—"JS Basics"hours—12
summary()—truewithin,falsewithjsCourse.hasOwnProperty
hasOwnProperty()—jsCoursecan call it because it's found here
For sqlCourse, which was overwritten by an assignment, hasOwnProperty("summary") returns true, so one line is enough to pick out just the overwritten courses. The table below shows how other ways of checking treat title and summary.
| Check | title (jsCourse's own) | summary (on Course.prototype) |
|---|---|---|
| Listed by Object.keys(jsCourse)? | Yes | No |
| Listed by for...in? | Yes | No |
| Course.prototype.hasOwnProperty("key") | false | true |
Adding Methods from Outside — Assigning to prototype
Now suppose the course instances are created by another team's shared code and handed to you, so you can change neither the class body nor the new lines. If you assign a display function that returns "Course: name" to each course one by one, calling it on any course you miss throws a TypeError.
If you assign a function to prototype, as in Course.prototype.label = function () { ... }, every instance finds the same label through the chain. If you can edit the class body, write the method there instead.
class Course {
constructor(title, hours) { this.title = title; this.hours = hours; }
summary() { return `${this.title} (${this.hours} hours)`; }
}
const jsCourse = new Course("JS Basics", 12); // Create two courses first
const sqlCourse = new Course("SQL Basics", 8);
// Assigning only to jsCourse leaves sqlCourse without label
jsCourse.label = function () { return `Course: ${this.title}`; };
console.log(typeof sqlCourse.label); // undefined (calling it throws a TypeError)
// Assign once to Course.prototype (written with function because it reads this)
Course.prototype.label = function () { return `Course: ${this.title}`; };
console.log(sqlCourse.label()); // Course: SQL Basics
console.log(sqlCourse.hasOwnProperty("label")); // false
// Collect the keys that for...in returns
const keys = [];
for (const key in sqlCourse) keys.push(key);
console.log(keys.join(", ")); // title, hours, label
It's found even for sqlCourse, which was created before the assignment, because the chain is searched at the moment the name is used. Unlike summary, which is written in the class body, a label added by assignment also shows up in for...in, so code that loops over keys gets it mixed in with the data keys.
Write Functions Added to prototype with function
If you write Course.prototype.label = () => this.title;, the arrow function uses the this from outside the class. In this site's editor, that's undefined, so sqlCourse.label() throws a TypeError. Use function instead.
Knowledge Check
Answer each question one by one.
Q2With LiveCourse extends Course, what is the prototype of LiveCourse.prototype?
Q3Some courses already exist. What happens if you then assign a label written with function to Course.prototype?