16 — Prototypes, Classes, and Iterators — The Object Model, Explained
"Classes are just sugar over prototypes" was the half-right summary I started with. The idea that everything else hangs off: JavaScript has exactly one inheritance mechanism — objects linked to other objects via a prototype chain — and class, iterators, and generators are all just syntax layered on top of that same chain. [1]
The framing that finally landed is the prototype as a fallback pointer. Every object has a hidden link to another object — its prototype. When I read a property that isn't on the object itself, JavaScript walks up the prototype chain looking for it, object by object, until it finds the property or reaches the end. That single mechanism — "if not here, ask my prototype" — is the entirety of JavaScript's inheritance. Methods shared by all instances live on the prototype once; each instance holds only its own data and a link upward.
Prototypes and prototypal inheritance
The mechanism in full: each object has an internal [[Prototype]] slot (exposed in most engines as __proto__, and settable via Object.getPrototypeOf/Object.setPrototypeOf). Reading obj.foo checks obj, then obj.__proto__, then its prototype, up to Object.prototype at the top of the chain [1][2]. Writing always sets a property directly on the object, never on the prototype — which is why shadows work: setting obj.foo = 5 on an instance creates an own foo that hides the prototype's foo.
Prototypal inheritance is just: create a new object whose prototype is an existing object [2]. Object.create(parent) does exactly this. The new object inherits the parent's properties for free, and can override them without touching the parent. There are no classes in this picture — just objects linked to objects. That's the whole model.
Classes: the syntax on top
The class keyword, added in ES6, is the ergonomic syntax for this exact mechanism [3]. It looks like classical inheritance from Java or Python, but it compiles down to prototypes:
class User {
constructor(name) { this.name = name; } // sets up own data
greet() { return `Hi, ${this.name}`; } // goes on User.prototype
static create(name) { return new User(name); } // goes on User itself
}
class Admin extends User { // wires Admin.prototype.__proto__ = User.prototype
constructor(name) { super(name); this.role = "admin"; }
ban(user) { /* … */ }
}extends links the prototypes; super calls the parent constructor or methods. Underneath, new User("Ave") creates an object whose __proto__ is User.prototype, exactly as if I'd done it by hand. The class is documentation and ergonomics; the prototype chain is what's actually running. That's why class is honestly called "syntactic sugar" — but the sugar matters, because writing extends and super is dramatically clearer than manually wiring Object.create chains.
Iterators: the protocol
Iteration in JavaScript is also built on a protocol — and the protocol is just an object with a .next() method [4]. The iterator protocol says: an iterator is any object with a next() function that returns { value, done }. The iterable protocol says: an iterable is any object with a [Symbol.iterator]() method that returns an iterator.
const evens = {
[Symbol.iterator]() {
let n = 0;
return {
next() {
n += 2;
return n > 10 ? { value: undefined, done: true } : { value: n, done: false };
}
};
}
};
for (const x of evens) console.log(x); // 2, 4, 6, 8, 10Arrays, strings, Maps, Sets, TypedArrays, and NodeLists all implement [Symbol.iterator], which is why for...of, the spread operator, and destructuring all work on them uniformly. The protocol is the contract; the loop constructs are just consumers of it.
Generators: iterators from a function
Writing an iterator by hand is verbose. Generators are functions that can pause and resume, and they produce iterators automatically [4]. The function* syntax and the yield keyword do it:
function* evens() {
for (let n = 2; n <= 10; n += 2) {
yield n; // pause here, yield the value, resume on next()
}
}
for (const x of evens()) console.log(x); // 2, 4, 6, 8, 10Each yield pauses the function and emits a value; calling .next() resumes from where it paused. Generators are the clean way to define lazy or infinite sequences — a generator yielding Fibonacci numbers doesn't compute the next one until asked. They're also the foundation of some async patterns (though async/await largely superseded them there).
call, apply, bind — explicit this again
These three Function.prototype methods show up in the same neighborhood because they're about reusing methods across objects [5][6][7]:
- fn.call(thisArg, ...args) — invoke now, this set, args individually.
- fn.apply(thisArg, [args]) — invoke now, this set, args as an array.
- fn.bind(thisArg, ...args) — return a new function with this (and optional preset args) permanently bound.
The connection to prototypes: function borrowing — using one object's method against another object by calling it through call/apply, without copying the method. Array.prototype.slice.call(arrayLikeThing) is the classic example of borrowing an array method for an array-like object (though Array.from is the modern equivalent). I covered the binding rules these implement in the this notes; here the point is that they're part of the same object model — functions are values, methods live on prototypes, and call/apply/bind let me invoke a method with any receiver I choose.
How I use this
Day to day, I write class for anything object-oriented — the prototype chain is doing the work, but the syntax keeps it readable, and extends/super are clearer than manual Object.create. For iteration, I reach for for...of and array methods, and I write a generator (or a [Symbol.iterator] method) only when I need a custom or lazy sequence. call/apply/bind are mostly for fixing this in callbacks — and even there, arrow functions have replaced most of my bind calls. The way of thinking that ties it together — one prototype-chain mechanism, with class/iterator/generator as layered syntax — is what I keep: when something behaves oddly, the answer is almost always "look at where the prototype chain actually points," and Object.getPrototypeOf(obj) answers that in one call.
References
[1] Mozilla, "Inheritance and the prototype chain," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
[2] I. Kantor, "Prototypes, inheritance," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/prototypes
[3] I. Kantor, "Classes," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/classes
[4] I. Kantor, "Iterables and iterators," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/iterable
[5] Mozilla, "Function.prototype.call()," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
[6] Mozilla, "Function.prototype.apply()," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
[7] Mozilla, "Function.prototype.bind()," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
Knowledge check · Question 1 of 5
What is the single inheritance mechanism in JavaScript?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!