Learn by reading through in order

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)
What a Class and Its Instances Point To
new Course("JS Basics", 12)class Coursenew Course("SQL Basics", 8)jsCourseCourse.prototypehas summarysqlCourseOwn keys:title, hoursOnly onesummary bodyNot its own, solooks inCourse.prototypejsCourse.prototypeis undefinedBoth summaryare the samesummary() givesSQL Basics(8 hours)
The prototype of both courses is the same as the class's prototype. summary, which they don't have themselves, is found there.

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 Order in Which summary Is Looked Up
liveCourse(instance)Has title, hoursNo summaryLiveCourse.prototypeHas scheduleNo summaryCourse.prototypeHas summaryFound hereObject.prototypeHas built-inmethods(next is null)
summary is searched for from the top and found in Course.prototype. The chain ends with null, right after Object.prototype.

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.

You'll display the method lookup order for each type of car. Car, ElectricCar, compactCar, ev, and prototypeNames are already declared.

① Display whether ev's prototype is ElectricCar.prototype.

② Define chainOf, which joins the names from a given car's prototype up to just before null.

③ Display the results for compactCar and ev.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

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
Where in and hasOwnProperty Look
Searched by "summary" in jsCourse (whole chain)
jsCourse itself (checked by jsCourse.hasOwnProperty)
  • title"JS Basics"
  • hours12
Course.prototype
  • summary()true with in, false with jsCourse.hasOwnProperty
Object.prototype
  • hasOwnProperty()jsCourse can call it because it's found here
in looks at the whole chain, while hasOwnProperty looks only inside the jsCourse box. summary isn't in the jsCourse box.

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.

Checktitle (jsCourse's own)summary (on Course.prototype)
Listed by Object.keys(jsCourse)?YesNo
Listed by for...in?YesNo
Course.prototype.hasOwnProperty("key")falsetrue

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
Where You Assign label Changes the Result
label assignedto jsCourse onlysqlCourse itselfhas no labelNot on the chaineither: undefinedsqlCourse.label()throws TypeErrorAssigned toCourse.prototype.labelsqlCourse itselfhas no labelFound onCourse.prototypeCourse: SQL Basics
In both cases, sqlCourse itself doesn't have label. It's found through the chain only when you assign it to Course.prototype.

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.

You'll add a running time display to a list of video titles without changing the body of Video. Video and videos are already declared.

① Add durationLabel, which every title can call and which returns "N h N min".

② For each title, display "Title: N h N min".

③ Display whether the first title has durationLabel itself.

④ Loop over the first title with for...in, and display the keys the title doesn't have itself.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1For a summary method written in a class, what does jsCourse.hasOwnProperty("summary") return?

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?