An empty array has a map method. A plain object has toString. An instance of your Course class has describe, even though you never put it there. All of these come from prototypes: every object can point to another object, and when a property is missing, JavaScript looks there instead. Classes are built on this mechanism, so understanding it explains how class, extends, instanceof and many puzzling bugs actually work.
Every object has a prototype
Each object has a hidden internal link, written in the specification as [[Prototype]], to another object or to null. You read it with Object.getPrototypeOf():
const course = { title: "JavaScript Basics" };
console.log(Object.getPrototypeOf(course) === Object.prototype); // → true
console.log(Object.getPrototypeOf(Object.prototype)); // → null
An object literal's prototype is Object.prototype, the object that provides toString, hasOwnProperty and the other methods every object seems to have. Object.prototype itself has no prototype, so the chain ends there.
How property lookup works
When you read obj.prop, the engine:
- Checks whether
objhas an own property calledprop. If so, it uses it. - If not, it moves to
obj's prototype and checks there. - It repeats up the chain until it finds the property or reaches
null, in which case the result isundefined.
console.log(course.title); // → JavaScript Basics (own property)
console.log(typeof course.toString); // → function (found on Object.prototype)
console.log(course.missing); // → undefined (not found anywhere)
Arrays work the same way, with one more step:
[1, 2, 3] ──> Array.prototype ──> Object.prototype ──> null
(own: 0, 1, 2, length) (map, filter, push...) (toString, hasOwnProperty...)
const numbers = [1, 2, 3];
console.log(Object.getPrototypeOf(numbers) === Array.prototype); // → true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // → true
console.log(Object.hasOwn(numbers, "map")); // → false
console.log(Object.hasOwn(Array.prototype, "map")); // → true
Every array shares one map function stored on Array.prototype. That is the whole point of prototypes: shared behaviour lives in one place, and each object stores only its own data.
Object.create: choosing a prototype
Object.create(proto) makes a new, empty object whose prototype is proto. It is the most direct way to see the chain in action:
const courseMethods = {
describe() {
return `${this.title}: ${this.lessons} lessons`;
},
};
const js = Object.create(courseMethods);
js.title = "JavaScript Basics";
js.lessons = 12;
console.log(js.describe()); // → JavaScript Basics: 12 lessons
console.log(Object.getPrototypeOf(js) === courseMethods); // → true
console.log(Object.keys(js)); // → [ 'title', 'lessons' ]
describe is found on courseMethods, one step up the chain. Inside it, this is still js, because this is set by how the method is called (js.describe()), not by where it was found. That rule, from lesson 21, is what lets one shared method work for every object.
Chains can be as long as you like:
const base = { greet() { return "hello"; } };
const middle = Object.create(base);
const leaf = Object.create(middle);
console.log(leaf.greet()); // → hello (found two steps up)
Objects with no prototype
Object.create(null) makes an object with no prototype at all. It has no toString, no hasOwnProperty, nothing:
const lookup = Object.create(null);
lookup.constructor = "a key like any other";
console.log("toString" in lookup); // → false
This used to be the standard way to build a safe dictionary from untrusted keys. Today a Map (lesson 37) is usually the better choice.
Own and inherited properties
Many bugs come from mixing up properties an object has itself with ones it inherits.
| Check | Includes inherited properties? |
|---|---|
Object.hasOwn(obj, key) | No, own properties only |
key in obj | Yes |
Object.keys(obj), Object.entries(obj) | No, own enumerable properties only |
for...in | Yes, own and inherited enumerable properties |
obj.key !== undefined | Yes, and it cannot tell "missing" from "set to undefined" |
console.log("describe" in js); // → true
console.log(Object.hasOwn(js, "describe")); // → false
Object.hasOwn() was added in ES2022 and is supported in all current browsers and Node.js. It replaces obj.hasOwnProperty(key), which fails on objects created with Object.create(null) and can be overwritten by a property of the same name.
Writing does not go up the chain
Reading walks the chain; writing does not. Assigning to a property creates or updates an own property on the object, even if the prototype has a property with that name:
const defaults = { theme: "light" };
const userSettings = Object.create(defaults);
console.log(userSettings.theme); // → light (inherited)
userSettings.theme = "dark"; // creates an own property
console.log(userSettings.theme); // → dark (own property shadows the inherited one)
console.log(defaults.theme); // → light (unchanged)
delete userSettings.theme;
console.log(userSettings.theme); // → light (inherited one visible again)
The own property shadows the inherited one. Two exceptions: if the prototype defines a setter for that name, the setter runs instead; and if the inherited property is read-only, the assignment fails (silently in sloppy code, with a TypeError in strict code).
One trap follows from this. Mutating an inherited object or array does not create an own property; it changes the shared one:
const shared = { tags: [] };
const a = Object.create(shared);
const b = Object.create(shared);
a.tags.push("css"); // reads tags (inherited), then mutates that one array
console.log(b.tags); // → [ 'css' ]
This is why data belongs on each instance and only methods belong on the prototype.
Constructor functions: the original pattern
Before class existed, objects with shared methods were built with constructor functions. Every ordinary function has a prototype property, an object that becomes the prototype of anything created with new:
function Course(title, lessons) {
this.title = title;
this.lessons = lessons;
}
Course.prototype.describe = function () {
return `${this.title}: ${this.lessons} lessons`;
};
const css = new Course("CSS Grid in Practice", 8);
console.log(css.describe()); // → CSS Grid in Practice: 8 lessons
console.log(Object.getPrototypeOf(css) === Course.prototype); // → true
new Course(...) does four things:
- Creates a new empty object.
- Sets its prototype to
Course.prototype. - Calls
Coursewiththisset to the new object. - Returns the new object (unless the function returns a different object itself).
Do not confuse the two "prototypes". Course.prototype is an ordinary property of the function, holding the object that instances will inherit from. It is not the prototype of Course itself, which is Function.prototype.
You will still find this pattern in older code and libraries.
What class really builds
Here is the Course class from lesson 36:
class Course {
constructor(title, lessons) {
this.title = title;
this.lessons = lessons;
}
describe() {
return `${this.title}: ${this.lessons} lessons`;
}
static fromJSON(data) {
return new Course(data.title, data.lessons);
}
}
console.log(typeof Course); // → function
console.log(Object.hasOwn(Course.prototype, "describe")); // → true
console.log(Object.hasOwn(Course, "fromJSON")); // → true
The class produced the same structure as the constructor function: a function called Course, instance methods on Course.prototype, and static methods as properties of the function itself. Class syntax is cleaner, and it adds some real differences:
- A class must be called with
new; calling it as a normal function throws aTypeError. - Methods are non-enumerable, so they do not show up in
for...inloops over instances. - The class body is always in strict mode, and the class is in the temporal dead zone until its declaration runs.
- Private
#fieldshave no equivalent in the constructor function pattern.
What extends builds
extends links two chains:
class VideoCourse extends Course {
constructor(title, lessons, minutes) {
super(title, lessons);
this.minutes = minutes;
}
}
const video = new VideoCourse("Flexbox Fundamentals", 6, 95);
// Instances: video -> VideoCourse.prototype -> Course.prototype -> Object.prototype
console.log(Object.getPrototypeOf(VideoCourse.prototype) === Course.prototype); // → true
// Statics: VideoCourse -> Course, so static methods are inherited too
console.log(Object.getPrototypeOf(VideoCourse) === Course); // → true
console.log(typeof VideoCourse.fromJSON); // → function
Tracing video.describe():
videohas own propertiestitle,lessonsandminutes. Nodescribe.VideoCourse.prototypehasconstructorand nothing else. Nodescribe.Course.prototypehasdescribe. Found; call it withthisset tovideo.
super.describe() inside a VideoCourse method simply starts the lookup at Course.prototype instead of at the object.
instanceof follows the chain
obj instanceof Fn asks: does Fn.prototype appear anywhere in obj's prototype chain?
console.log(video instanceof VideoCourse); // → true
console.log(video instanceof Course); // → true
console.log(video instanceof Object); // → true
console.log(Object.create(null) instanceof Object); // → false
That is why an error created by your HttpError class from lesson 35 is also instanceof Error. It also explains a surprise: objects from another window or iframe have a different Array.prototype, so instanceof Array is false for them. Array.isArray() works across windows, which is why it exists.
Things to avoid
Changing built-in prototypes
You can add methods to Array.prototype or String.prototype, and every array or string in the page gets them. Do not. If a future version of JavaScript adds a method with the same name, your version and the standard one collide. This has happened: a widely used library had added its own Array.prototype.flatten, and the standard method had to be named flat instead to avoid breaking sites. Write a plain function instead.
Changing an object's prototype after creation
Object.setPrototypeOf(obj, proto) exists, but changing the prototype of an existing object makes engines discard optimisations for it and for code that touches it. Set the prototype when you create the object, with Object.create or a class. Lesson 51 explains why engines care.
The legacy __proto__ accessor
Older code reads and writes obj.__proto__. It is kept only for compatibility. Use Object.getPrototypeOf() and Object.create() instead.
Prototype pollution is a real class of security bug. Code that deep-merges untrusted JSON into an object, following keys blindly, can be tricked by a key named __proto__ into writing to Object.prototype, which changes every object in the program. When copying keys from untrusted data, skip __proto__, constructor and prototype, or store the data in a Map.
Summary
- Every object has a
[[Prototype]]link to another object ornull; read it withObject.getPrototypeOf(). - Reading a property walks the chain until it is found or the chain ends. Writing creates an own property that shadows the inherited one.
Object.create(proto)creates an object with a chosen prototype;Object.create(null)creates one with none.Object.hasOwn()andObject.keys()see own properties;inandfor...inalso see inherited ones.new Fn()creates an object whose prototype isFn.prototype, then runsFnwiththisset to it.classputs methods onClass.prototypeand statics on the class;extendslinks both the prototype chain and the static chain.instanceofchecks whether a prototype appears in an object's chain.- Keep data on instances and methods on prototypes, never modify built-in prototypes, and guard against prototype pollution.