Class basics
Learn JavaScript class syntax, constructors, prototype methods, getters, setters, public fields, function-like class values, and the async factory pattern.
- 01Build instancesWrite a class with fields, a constructor, and methods.
- 02Trace where behavior livesTell own fields from shared prototype methods.
- 03Use modern patternsAdd accessors and a static async factory without making constructors async.
Classes in one picture
A JavaScript class is a compact way to describe objects that share the same shape and behavior. If constructor functions taught you how new links an object to F.prototype, class syntax is the same idea in a cleaner uniform.
Think of a class as a blueprint. Each time you use new, JavaScript builds one house from the blueprint. The constructor is the move-in checklist run once for that house. Prototype methods live in a shared blueprint library, so they are not copied into every house. Public fields are furniture each house receives for itself.
The blueprint tells builders where rooms go. It does not mean every house has a private copy of the blueprint book. Likewise, a class tells JavaScript how to create instances while shared methods stay on the prototype.
- In real life: Blueprint
- In JavaScript: The
class Counter { ... }declaration - In real life: A new house
- In JavaScript: One object made by
new Counter() - In real life: Move-in checklist
- In JavaScript: The
constructorbody - In real life: Shared blueprint library
- In JavaScript:
Counter.prototypemethods - In real life: Furniture in each house
- In JavaScript: Public fields and
this.valuedata
Where the analogy stops: A real blueprint is paper, but a JavaScript class is a real runtime value: typeof Counter is function, and you can pass it around like other functions.
This lesson covers class syntax, constructors, prototype methods, class accessors, public fields, classes as function values, and the static async factory pattern. Later lessons go deeper into inheritance, static blocks, private fields, and advanced object-oriented design.
Class syntax and the constructor
STEP THROUGHA class declaration starts with class, a name, and a body. Inside the body, constructor is the special method that runs when you write new ClassName(...). It receives the arguments from new, and this is the fresh instance being built.
Base-class public fields run before the constructor body. The experiment proves it: history exists before the constructor pushes its second note.
Classes build objects in a predictable order. Step through a new Counter and watch fields, constructor code, and prototype lookup happen.
script
class Counter { constructor(start = 0) { this.value = start; this.history.push("constructor ran"); } increment() { this.value += 1; return this.value; }} const counter = new Counter(5);const result = counter.increment();console.log(counter.history.join(" → "));console.log(result);Under the hood, new Counter(5) still creates an object, links it to Counter.prototype, runs setup with this, and returns the object. The syntax prevents common constructor-function mistakes and keeps methods non-enumerable by default.
Methods live on the prototype
INTERACTIVEWhen you write a method such as increment() in a class body, JavaScript puts that function on Counter.prototype. Instances do not receive their own copy. This is exactly the sharing model from F.prototype & constructors, with friendlier syntax.
The next playground compares a class and a constructor function side by side. Both create objects that greet the same way; the differences are the safer rules classes add.
class UserClass { constructor(name) { this.name = name; } greet() { return "Hi " + this.name; }} function UserFunction(name) { this.name = name;}UserFunction.prototype.greet = function () { return "Hi " + this.name;}; const classUser = new UserClass("Ada");const functionUser = new UserFunction("Ada");This result comes from real class and constructor values created by the lesson code.
Class instance behavior → Hi Ada
class UserClass { constructor(name) { this.name = name; } greet() { return "Hi " + this.name; }} function UserFunction(name) { this.name = name;}UserFunction.prototype.greet = function () { return "Hi " + this.name;}; const classUser = new UserClass("Ada");const functionUser = new UserFunction("Ada");| Question | Class | Constructor function |
|---|---|---|
| Creation | new UserClass("Ada") | new UserFunction("Ada") |
| Shared methods | Method syntax creates non-enumerable prototype methods | Assignment creates enumerable methods unless you define descriptors |
Calling without new | Throws TypeError | May run like an ordinary function in sloppy scripts |
| Strictness | Class bodies are always strict | Ordinary functions depend on where/how they are written |
| Hoisting | Class declarations have a temporal dead zone | Function declarations are callable before their line |
clicks = 0this.value = startincrement() { ... }get fahrenheit() { ... }static async create()typeof Counter
Sort each class feature by where it lives.
Getters, setters, and public fields
INTERACTIVEA getter is a method you read like a property. A setter is a method you assign like a property. They are useful when the object should store one source of truth but expose a friendlier view.
class Temperature { constructor(celsius) { this.celsius = celsius; } get fahrenheit() { return this.celsius * 9 / 5 + 32; } set fahrenheit(value) { this.celsius = (value - 32) * 5 / 9; }} const tea = new Temperature(100);console.log(tea.fahrenheit);tea.fahrenheit = 32;console.log(tea.celsius);Getter reads 100°C as 212.0°F. The setter converts Fahrenheit back into celsius and stores only celsius.
celsius) and computes the other view when asked.Public fields are different: they create own properties on each instance. A field can hold data, an array, or even an arrow function. That arrow function field is handy when you need a callback that keeps this, but it is not shared through the prototype.
class ToolbarButton { clicks = 0; handleClick = () => { this.clicks += 1; return this.clicks; }; describe() { return "Button clicked " + this.clicks + " time(s)"; }} const save = new ToolbarButton();const cancel = new ToolbarButton();console.log(save.describe === cancel.describe);console.log(save.handleClick === cancel.handleClick);A method declaration is shared through the prototype; the arrow function field is created separately for each instance.
Fields are like furniture delivered to each house. Prototype methods are like books in a library all houses can use. If you put the whole library in every house, it works, but it wastes space.
- In real life: Every house gets its own sofa
- In JavaScript: Every instance gets its own public fields
- In real life: The town library is shared
- In JavaScript: Prototype methods are shared
- In real life: Buying a sofa for every house costs more
- In JavaScript: Arrow function fields allocate a function per instance
Where the analogy stops: Furniture is visible; object properties can be non-enumerable, inherited, or hidden behind accessors. Use the analogy only for ownership, not visibility.
Classes are functions, and async setup uses factories
INTERACTIVEA class declaration creates a value. In fact, typeof Counter is "function". The value has a prototype, can be exported, and can have static methods. But it cannot be called like Counter(); classes must be constructed with new.
Constructors should do synchronous setup. If creating an object needs to wait for data, use a static async factory such as Profile.create(). The factory can await a simulated loading step, then call the normal constructor and return a ready instance.
class Profile { constructor(name, avatar) { this.name = name; this.avatar = avatar; } static async create(name) { const avatar = await loadAvatar(name); return new Profile(name, avatar); }} async function loadAvatar(name) { return name.slice(0, 1).toUpperCase() + "-avatar";} const profile = await Profile.create("Ada");console.log(profile.avatar);Not run yet
Not run yet
The factory pattern uses async and await, which the asynchronous JavaScript stage covers in depth. For now, remember the shape: static async create() waits, then returns new ClassName(...).
Where you’ll use this
Classes are useful when many objects share behavior and each object carries its own state: UI widgets, model objects, data validators, game entities, or service clients. A class makes the shared API easy to find: constructor for required data, methods for actions, getters for computed views, and static factories for named creation paths.
A ShoppingCart might store its own items field, share add() and total() on the prototype, expose get count(), and offer static async fromSavedCart(userId) when it needs to load saved data first.
Classes are not mandatory. Plain objects and factory functions are still great. Choose a class when the new story helps readers understand that many instances share one set of behaviors.
Common misconceptions
“Classes replace prototypes.”
No. Class syntax uses prototypes. Methods and accessors still live on ClassName.prototype.
“The constructor creates methods for each object.”
The constructor initializes instance data. Method declarations are shared.
“Fields and methods are the same.”
Fields are own properties created per instance. Methods are prototype properties unless you intentionally store a function in a field.
“Getters store data.”
A getter runs code when read. It may compute from stored data and does not need to store its own value.
“Constructors can be async.”
A constructor returns the new object immediately. Use a static async factory when setup must wait.
“Classes are hoisted like function declarations.”
Class declarations are in the temporal dead zone until the declaration runs.
Practice exercises
5 EXERCISESWrite the class shown in the starter and predict the two console lines.
class TodoItem {
constructor(text) {
this.text = text;
this.done = false;
}
complete() {
this.done = true;
return this.text + " done";
}
}
const task = new TodoItem("Ship lesson");
console.log(task.complete());
console.log(task.done);class TodoItem {
constructor(text) {
this.text = text;
this.done = false;
}
complete() {
this.done = true;
return this.text + " done";
}
}
const task = new TodoItem("Ship lesson");
console.log(task.complete());
console.log(task.done);The constructor stores instance data. The method is shared, but it changes the one instance through this.
Predict both booleans without running the program.
class Counter {
count = 0;
increment() {
this.count += 1;
return this.count;
}
}
const a = new Counter();
const b = new Counter();
console.log(a.increment === b.increment);
console.log(a.count === b.count);It prints true and true. The method function is shared, and before any calls both own count fields hold 0.
This code throws. Fix the creation line so it prints the widget name.
class Widget {
constructor(name) {
this.name = name;
}
}
const widget = Widget("Panel");
console.log(widget.name);class Widget {
constructor(name) {
this.name = name;
}
}
const widget = new Widget("Panel");
console.log(widget.name);Classes must be constructed. new Widget("Panel") creates the instance before the constructor stores name.
Add a getter that converts Celsius to Fahrenheit.
class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 9 / 5 + 32;
}
}
const room = new Temperature(20);
console.log(room.fahrenheit);class Temperature {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 9 / 5 + 32;
}
}
const room = new Temperature(20);
console.log(room.fahrenheit);Reading room.fahrenheit calls the getter and computes 68 from 20°C.
Use a static async factory to create a ready profile, then print its avatar.
class Profile {
constructor(name, avatar) {
this.name = name;
this.avatar = avatar;
}
static async create(name) {
const avatar = name.slice(0, 1).toUpperCase() + "-avatar";
return new Profile(name, avatar);
}
}
Profile.create("Ada").then(profile => console.log(profile.avatar));class Profile {
constructor(name, avatar) {
this.name = name;
this.avatar = avatar;
}
static async create(name) {
const avatar = name.slice(0, 1).toUpperCase() + "-avatar";
return new Profile(name, avatar);
}
}
Profile.create("Ada").then(profile => console.log(profile.avatar));The static factory waits for the simulated async step, then returns a normal instance. The resolved profile prints A-avatar.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7What is a JavaScript class?
Choose an answer to see the explanation.
Question 2 of 7What does this method-sharing code print?
Read the code, then predictclass Counter { value = 0; increment() { this.value += 1; return this.value; } } const a = new Counter(); const b = new Counter(); console.log(a.increment === b.increment);Choose an answer to see the explanation.
Question 3 of 7Which runs before the constructor body in a base class?
Choose an answer to see the explanation.
Question 4 of 7What does this getter code print?
Read the code, then predictclass Temperature { constructor(celsius) { this.celsius = celsius; } get fahrenheit() { return this.celsius * 9 / 5 + 32; } } console.log(new Temperature(0).fahrenheit);Choose an answer to see the explanation.
Question 5 of 7Which statement about classes is true?
Choose an answer to see the explanation.
Question 6 of 7Why use
static async create()?Choose an answer to see the explanation.
Question 7 of 7What does this arrow-field comparison print?
Read the code, then predictclass Button { click = () => "clicked"; describe() { return "button"; } } const one = new Button(); const two = new Button(); console.log(one.click === two.click);Choose an answer to see the explanation.
Key takeaways
- A class is syntax for creating objects with shared prototype behavior.
- The constructor runs once per
newinstance; public fields initialize before a base constructor body. - Methods and accessors live on the prototype; fields live on each instance.
- Classes are function values, but they must be called with
newand live in the TDZ before declaration. - Use
static async create()when setup must wait before returning a ready instance.
Remember the one-liner.
A class is a blueprint whose constructor sets up each instance while prototype methods are shared by all instances.
Up next: Inheritance with extends & super.