Free lesson · JavaScript Basic to Advance
Classes: Syntactic Sugar Over Prototypes
A New Voice, the Same Mechanism
Hi, I'm Brandon Carter. I'm a Principal SDE at Amazon Web Services, working on distributed storage infrastructure at S3. I'm also the Head of Curriculum at AlgoInsight and the Amazon Bar Raiser who's interviewed roughly 300 candidates over the past few years. Object-oriented design is my specialty — I own the OO Design course on this platform — so I'll be taking you through Topics 8.2 through 8.6 on classes.
The reason for the handoff: Nikhil owns the runtime model material (execution contexts, closures, this). I own OO design. Classes are where those two areas meet, and the strategy is for each of us to lead the section where we have the most credible experience.
What I want you to internalize from this Topic: classes in JavaScript are syntactic sugar over the prototype mechanism Nikhil covered in Module 7. They look like classes from other languages. They feel familiar if you've used Java, C++, or Python. But underneath, they're using the same prototype chain we built in Topic 7.5 with Object.create.
// Without classes — using the constructor function pattern from Topic 7.6
function UserOld(name, age) {
this.name = name;
this.age = age;
}
UserOld.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
// With classes — modern syntax
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name}`;
}
}
const olenaOld = new UserOld("Olena", 35);
const olenaNew = new User("Olena", 35);
console.log(olenaOld.greet()); // "Hi, I'm Olena"
console.log(olenaNew.greet()); // "Hi, I'm Olena"
// Both use the same mechanism
console.log(typeof UserOld); // "function"
console.log(typeof User); // "function" — yes, classes are functions
console.log(Object.getPrototypeOf(olenaNew) === User.prototype); // true
Same behavior. Same underlying mechanism. The class version just looks cleaner.
Why Classes Were Added
JavaScript existed for 20 years without class syntax. People built object-oriented systems using prototypes and constructor functions, and it worked. So why were classes added in ES6?
Three reasons:
1. Familiarity. Developers coming from other languages expected class syntax. Forcing them to learn prototypal inheritance before writing their first object-oriented JavaScript was a barrier to adoption.
2. Cleaner syntax. Compare the two versions above. Even for someone fluent with prototypes, the class version is shorter and clearer. There's less ceremony, less repetition.
3. Standardization of common patterns. Inheritance via constructor functions had many subtly different conventions in pre-ES6 code. The class syntax gave the community one standard way to do it, eliminating the confusion of "which inheritance pattern is this library using?"
But — and this is the part that matters for understanding — the class syntax did not change the underlying mechanism. JavaScript still uses prototypal inheritance. Classes are just a cleaner way to express it.
When a class behaves unexpectedly, the explanation is usually at the prototype level. When you read advanced JavaScript code that does something unusual with classes, the unusual part is usually exploiting the fact that they're prototypes underneath. Keep that mental model.
Interactive animation: A constructor function with greet on User.prototype beside the class that builds the same object graph.
The Anatomy of a Class
A class declaration has several parts:
class User {
// Instance fields (modern syntax)
status = "active";
// Constructor — runs when 'new User()' is called
constructor(name, age) {
this.name = name;
this.age = age;
}
// Instance methods
greet() {
return `Hi, I'm ${this.name}`;
}
birthday() {
this.age++;
return this.age;
}
// Getter — runs when 'instance.fullDescription' is read
get fullDescription() {
return `${this.name}, ${this.age}, ${this.status}`;
}
}
const olena = new User("Olena", 35);
console.log(olena.name); // "Olena"
console.log(olena.status); // "active"
console.log(olena.greet()); // "Hi, I'm Olena"
console.log(olena.fullDescription); // "Olena, 35, active"
The parts:
class ClassName { }— declares the class- Instance fields — properties initialized on every instance (
status = "active") constructor— the function that runs whennew ClassName(...)is called- Instance methods — functions defined inside the class, available on every instance
- Getters and setters — methods that look like properties when accessed
All of these compile down to the constructor function + prototype pattern from Topic 7.6. The class syntax just makes the structure obvious at a glance.
Interactive animation: Interrogating a class - typeof User prints "function" and the instance links straight to User.prototype.
What new Does (Revisited)
We covered this in Topic 7.8 when implementing new from scratch. The four steps remain the same with classes:
- A new empty object is created
- The new object's prototype is set to
ClassName.prototype - The constructor function runs with
thisbound to the new object - The new object is returned
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hi, I'm ${this.name}`;
}
}
const olena = new User("Olena");
// Verify the four steps
console.log(olena instanceof User); // true
console.log(Object.getPrototypeOf(olena) === User.prototype); // true
console.log(typeof User); // "function" — classes are functions
console.log(User.prototype.greet); // [Function: greet]
Every method you define inside a class ends up on ClassName.prototype. Every instance you create with new is linked to that prototype through the chain. This is exactly the same as the constructor pattern from Topic 7.6, just written more cleanly.
Interactive animation: What new User() wires - data on the instance, greet found one link up on User.prototype.
Class Expressions
Just like functions, classes can be declared (named) or used as expressions:
// Class declaration
class User {
constructor(name) {
this.name = name;
}
}
// Class expression — assigned to a variable
const UserExpression = class {
constructor(name) {
this.name = name;
}
};
// Named class expression — name is internal
const UserNamed = class InternalName {
constructor(name) {
this.name = name;
}
};
const u1 = new User("Olena");
const u2 = new UserExpression("Nikhil");
const u3 = new UserNamed("Emeka");
console.log(u1.name);
console.log(u2.name);
console.log(u3.name);
Class expressions are useful when you want to assign a class to a variable, pass it to a function, or define it conditionally. The named version (with InternalName) makes the name available for self-reference inside the class, but not outside.
In practice, you'll mostly use class declarations. Class expressions appear in libraries, mixins, and some advanced patterns.
graph TD
A[class User] --> B[User constructor function]
A --> C[User.prototype object<br/>holds methods]
D[new User] --> E[Empty object created]
E --> F[Prototype set to User.prototype]
F --> G[Constructor runs<br/>with this = new object]
G --> H[Instance returned]
style B fill:#7c3aed,color:#fff
style C fill:#10b981,color:#fff
style H fill:#3b82f6,color:#fff
This diagram shows what happens when you define and instantiate a class. The class declaration creates a constructor function and a prototype object. The prototype holds all the methods defined in the class. When you call new on the class, JavaScript creates an empty object, sets its prototype to the class's prototype, runs the constructor function with this bound to the new object, and returns the instance.
Strict Mode by Default
Class bodies run in strict mode automatically, even if the surrounding code isn't in strict mode. This is part of the standardization classes provide — they're always strict.
class User {
constructor() {
this.name = "Olena";
}
detachedTest() {
return this.name;
}
}
const olena = new User();
const detached = olena.detachedTest;
console.log(olena.detachedTest()); // "Olena"
try {
console.log(detached()); // strict mode: throws because this is undefined
} catch (e) {
console.log(`Error: ${e.message}`);
}
In strict mode, calling a method without an object context produces this === undefined, and trying to read undefined.name throws. In non-strict mode, the same code would silently use the global object as this. Class methods get the safer strict-mode behavior by default.
Common Pitfalls
Warning: The class basics traps.
Forgetting
new. Calling a class withoutnewthrows a TypeError.User("Olena")doesn't work — you neednew User("Olena"). This is a feature, not a bug. It prevents the constructor from accidentally being called as a regular function.Assuming classes work like classes in other languages. They look similar but use prototypal inheritance underneath. Some patterns that work in Java or C++ have different semantics in JavaScript. Always trace through the prototype mechanism when something behaves unexpectedly.
Defining methods incorrectly. Methods go inside the class body without
functionand without commas between them. Addingfunctionor commas is a syntax error.
Why This Matters in Interviews
Interview: "What's the relationship between classes and prototypes in JavaScript?" is a senior-level question. The expected answer: classes are syntactic sugar over prototypes;
class User { method() {} }is equivalent to defining a constructor function and adding the method toUser.prototype. Demonstrating this withtypeof Userreturning"function"shows depth.The next-level question: "Can you build something equivalent to a class using only
functionandObject.create?" Yes — and Topic 7.6 covered exactly that pattern.Topic 8.7 — Interview Lens: this, Classes, and OO Patterns — covers the deeper interview treatment.
Try It
Exercise: Take this constructor-function pattern from Topic 7.6 and rewrite it as a modern class:
function Account(initialBalance) { this.balance = initialBalance; } Account.prototype.deposit = function(amount) { this.balance += amount; return this.balance; }; Account.prototype.withdraw = function(amount) { if (amount > this.balance) return null; this.balance -= amount; return this.balance; }; Account.prototype.getBalance = function() { return this.balance; };Then verify that the class version produces identical results, and that
typeof YourClass === "function". Both representations should behave the same — that's the whole point of classes being syntactic sugar.