Class basics
Learn JavaScript class syntax, constructors, prototype methods, getters, setters, public fields, function-like class values, and the async factory pattern.
- 01Write a classWrite a class with fields, a constructor, and methods, and make objects from it with
new. - 02Tell where each part is storedSay which parts each object gets for itself, and which parts all objects share on the prototype.
- 03Use getters, setters, and static methodsAdd a getter and a setter, and use a static async factory when setup must wait for data.
Classes in one picture
A class is a plan for making objects. Every object made from the same class has the same kind of data and the same methods. An object made from a class is called an instance of that class.
In the last lesson, you made objects with a constructor function and new. A class does the same job with shorter, clearer code.
class User { constructor(name) { this.name = name; } greet() { return "Hi " + this.name; }} const user = new User("Asha");console.log(user.greet());Here is what each part does:
- Line 1 starts a class called
User. - Lines 2 to 4 are the constructor. It runs once for each new object. It stores the name on the new object as
this.name. - Lines 5 to 7 are a method called
greet(). It reads the object's name throughthis. - Line 10 makes one instance with
new User("Asha"). - Line 11 calls
greet()and printsHi Asha.
The name and the method end up in two different places. The name is stored inside user. The method is stored once on User.prototype, a shared object that every User instance links to.
new User("Asha") gives user its own name. The greet() method is not inside user; it sits on User.prototype, and user links to it.Think of a class as a blueprint for a house. Each new builds one house from it.
A builder reads the blueprint and builds a house. Then the move-in checklist runs once for that house. Each house gets its own furniture. The houses all use one town library, so no house needs its own copy of every book.
- In real life: Blueprint
- In JavaScript: The
class Counter { ... }code - 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 town library
- In JavaScript: Methods on
Counter.prototype - In real life: Furniture in each house
- In JavaScript: Fields and
this.valuedata
This lesson covers these parts of a class:
- The class code and the constructor.
- Methods, which all instances share.
- Static members, which belong to the class itself.
- Getters, setters, and fields.
- Classes as values, and a safe way to set up an object that must wait for data.
Later lessons cover classes that extend other classes, private fields, and more.
The class code and the constructor
STEP THROUGHA class starts with the word class, then a name, then a body in curly braces { }. Inside the body, the method named constructor is special:
- It runs when you write
new ClassName(...). - It gets the arguments you pass to
new. - Inside it,
thisis the new object being built.
A class can also have fields. A field is a line like history = ["field initialized"]; in the class body. It gives every new object a property with a starting value.
Fields run before the constructor body. The experiment below shows it. The history field makes an array first. Then the constructor body adds a second note to that array.
history already exists when the constructor body runs. Then new hands the finished object to counter.A class builds each object in the same order. Step through new Counter and watch the field, then the constructor body, then the method call.
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);class Score { constructor(player) { this.player = player; this.points = 0; } addPoint() { this.points += 1; }} const first = new Score("Asha");const second = new Score("Ben");first.addPoint();console.log(first.points);console.log(second.points);The constructor gives each instance its own player and its own points. Line 13 calls addPoint() on first, so only first.points changes. The program prints 1, then 0.
class Order { constructor(customer = "Guest") { this.customer = customer; }} const order = new Order();console.log(order.customer);The = "Guest" part is a default value. It is used only when the caller passes nothing. Line 7 calls new Order() with no argument, so customer is Guest. If you passed a name, the object would store that name instead.
new Counter(5) does the same steps as with a constructor function:
- It makes a new, empty object.
- It links the object to
Counter.prototype. - It runs the setup code with
thisset to the object. - It gives back the object.
The class version adds safety rules. For example, it stops you from forgetting new.
Methods live on the prototype
INTERACTIVEWhen you write a method such as increment() in a class body, JavaScript puts that one function on Counter.prototype. The instances do not get their own copies. They all find the method through their link to the prototype. This is the same sharing you saw in F.prototype & constructors.
The next playground shows a class and a constructor function side by side. Both make objects that greet in the same way. Click the buttons to see where the two are different.
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");The class object greets by name: classUser.greet().
Class: greet() → 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");To check where a property is stored, use Object.hasOwn(object, "name"). It gives true only when the property sits directly on that object, not on its prototype.
class User { role = "reader"; greet() { return "Hi"; }} const user = new User();console.log(Object.hasOwn(user, "role"));console.log(Object.hasOwn(user, "greet"));console.log(Object.hasOwn(User.prototype, "greet"));Line 2 is a field, so every User gets its own role. The three checks print:
- Line 10 prints
true:userhas its ownrole. - Line 11 prints
false:userdoes not have its owngreet. - Line 12 prints
true:greetis stored onUser.prototype.
Object.keys(object) gives an array of the names an object lists as its own. A property that shows up in this list is called enumerable. Class methods are not enumerable, so they don't show up.
class User { role = "reader"; greet() { return "Hi"; }} const user = new User();console.log(Object.keys(user).join(","));console.log(Object.keys(User.prototype).join(",") || "none");Line 9 prints role, the field. Line 10 prints none: the list for User.prototype is empty, so the || "none" part fills in the word. A for...in loop can also list enumerable properties from the prototype, so use Object.keys when you want only the object's own names.
"Not enumerable" does not mean "private". Any code can still call the method through the prototype, and any code can still read a field. Private fields use a different #name form, which a later lesson covers.
The table sums up the differences. Read each row as a question, then compare the two answers.
| What you check | With a class | With a constructor function |
|---|---|---|
| How do you make an object? | You write new UserClass("Ada"). | You write new UserFunction("Ada"). |
Does Object.keys list the shared methods? | No. Class methods are not enumerable. | Yes. A method added with = is enumerable. |
What if you forget new? | It throws a TypeError. | Outside strict mode, it runs like a normal function and makes no object. |
| Is the code in strict mode? | Yes, always. | Only if the file or function asks for strict mode. |
| Can you use it above the line that makes it? | No. It throws a ReferenceError. | Yes. A function declaration works anywhere in its scope. |
clicks = 0this.value = startincrement() { ... }get fahrenheit() { ... }static async create()typeof Counter
Sort each line of class code by where it ends up.
Static members belong to the class
CLASS VALUEA static member is a field or method with the word static in front. It is stored on the class itself, not on the instances. So you call it on the class name: Cart.withTax(), not cart.withTax().
Static members are good for two things:
- Rules that are the same for every instance, like a tax rate.
- Named ways to make an instance, which you'll see later in this lesson.
class Cart { static taxRate = 0.05; constructor(total) { this.total = total; } static withTax(total) { return total * (1 + Cart.taxRate); }} const cart = new Cart(20);console.log(cart.total);console.log(Cart.withTax(cart.total));Here is what each part does:
- Line 2 stores one tax rate,
0.05, on the classCart. - Lines 8 to 10 are a static method. It reads the tax rate and works out a price. It does not need any particular cart.
- Line 13 makes one cart with its own
totalof20. - Line 14 prints
20. Line 15 prints21, which is 20 plus 5 percent tax.
taxRate and withTax() are on the class Cart. The object cart only holds its own total, so cart.withTax(20) fails.class Cart { static withTax(total) { return total * 1.05; }} const cart = new Cart();console.log(cart.withTax(20));The last line throws a TypeError, the error JavaScript gives when you use a value in a way it can't be used. cart is an instance, and it has no withTax method. To fix it, call Cart.withTax(20) on the class.
If a method needs one object's data, like this.total, make it a normal method. If it needs only the arguments you pass and rules for the whole class, a static method can be clearer.
Getters, setters, and fields
INTERACTIVEA getter is a method that you read like a property. You write it with get in front, and you read it without brackets: tea.fahrenheit. Each read runs the method.
A setter is a method that runs when you assign to a property. You write it with set in front, and you use it with =: tea.fahrenheit = 32.
Together they let an object store one number but show it in more than one way. In the playground, the object stores only Celsius. The getter works out Fahrenheit each time you read it.
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);The getter reads 100°C as 212.0°F. The setter turns a Fahrenheit number back into Celsius, and stores only Celsius.
celsius. The getter works out Fahrenheit each time you read it.class Score { constructor(value) { this.value = value; } get passed() { return this.value >= 50; } set passed(next) { this.value = next ? 50 : 0; }} const score = new Score(42);console.log(score.passed);score.passed = true;console.log(score.value);Here is what each part does:
- Lines 6 to 8 are the getter. It turns the stored number into
trueorfalse: did the score reach 50? - Lines 10 to 12 are the setter. It takes
trueorfalseand stores 50 or 0. - Line 16 reads
score.passed. 42 is less than 50, so it printsfalse. - Line 17 assigns
true, so the setter stores 50. Line 18 prints50.
Fields work differently. Each field becomes a property on each instance. A field can hold a number, an array, or even an arrow function.
An arrow function in a field is handy for a callback, a function you pass to other code to call later, such as a button click. It always uses the instance as this. But each instance gets its own copy of that function.
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 is stored once on the prototype and shared. An arrow function field is made again for each object.
handleClick, because it is a field. Both buttons share one describe(), because it is a method on the prototype.Fields are like furniture delivered to each house. Methods are like books in the town library that every house can use. You could put a copy of every book in every house. It would work, but it would take up a lot of room.
- In real life: Every house gets its own sofa
- In JavaScript: Every instance gets its own fields
- In real life: The town library is shared
- In JavaScript: Methods on the prototype are shared
- In real life: A sofa for every house costs more
- In JavaScript: An arrow function field makes one function per instance
Each new instance runs its field lines again. So a field such as items = [] makes a new, empty array for every cart. No two carts share one list by accident.
class Cart { items = [];} const first = new Cart();const second = new Cart();first.items.push("tea");console.log(first.items.length);console.log(second.items.length);Line 7 adds "tea" to first's array only. The program prints 1, then 0. The two carts have two separate arrays.
Use an arrow function field when a callback really needs to keep the instance as this. Otherwise, a normal method is the better choice, because all instances share one copy.
Classes are functions, and waiting setup uses a factory
INTERACTIVEA class is a value, like a number or a function. In fact, typeof Counter gives "function". (typeof tells you what kind of value something is.) So you can do these things with a class:
- Store it in a variable.
- Export it from a file.
- Pass it to another function.
But a class has one rule a normal function doesn't: you can't call it as Counter(). You must use new Counter().
const Guest = class { constructor(name) { this.name = name; } greeting() { return "Hello, " + this.name; }}; const guest = new Guest("Asha");console.log(guest.greeting());console.log(typeof Guest);This code is a class expression: a class with no name of its own, stored in the variable Guest. After line 9, it works like any other class. Line 12 prints Hello, Asha. Line 13 prints function.
- Calling a class without
newthrows aTypeError. - Using a class name above the line that makes the class throws a
ReferenceError, the error for a name you can't use yet. This time before the line runs is called the temporal dead zone. - Code in a class always runs in strict mode, JavaScript's safer rule set. So if you copy a method out of its object and call it alone,
thisisundefined. Readingthis.namethen throws an error.
A class expression is handy when you need a class in only one place, or want to pick one of two classes, or pass a class into a function. It follows the same rules as any class.
Now for objects that must wait for data before they are ready. new Profile() always gives back the object right away, so a constructor can't wait. Instead, use a static async factory:
- A factory is a function that makes and returns an object.
- Static means it is stored on the class, like
Profile.create(). - Async means it can wait, using
await, before it returns.
The factory waits for the data. Then it calls the normal constructor and returns the ready object. Anyone reading await Profile.create(...) can see that the code waits.
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";} async function main() { const profile = await Profile.create("Ada"); console.log(profile.avatar);} main();Not run yet
Not run yet
The words async and await get their own lessons later in the course. For now, remember the pattern: static async create() waits, then returns new ClassName(...).
Where you’ll use this
A class helps when you have many objects that do the same things, but each one keeps its own data. Some examples:
- Buttons, menus, and other parts of a page.
- Shopping carts, users, and orders.
- Players and enemies in a game.
- Code that checks form input, or code that talks to a server.
A class also keeps the parts easy to find:
- The constructor takes the data the object needs.
- Methods do the actions.
- Getters work out values from the stored data.
- Static factories give you named ways to make an object.
A ShoppingCart class might keep its own items field. It might share add() and total() methods on the prototype, and have a getter, get count(). If it must load a saved cart first, it might have static async fromSavedCart(userId).
class ShoppingCart { items = []; add(item) { this.items.push(item); } get count() { return this.items.length; }} const cart = new ShoppingCart();cart.add("tea");cart.add("bread");console.log(cart.count);Each cart keeps its own items list. All carts share one add() method. The count getter works out how many items there are. After two calls to add(), line 16 prints 2.
If your program has only one cart, a plain object may be simpler. A class is worth it when you need many carts that behave the same way.
Pick a class when an object has a clear life: it is made, it changes over time, and several actions belong to it. The constructor can refuse missing data. Methods can keep the data correct. A getter can show a useful value without letting other code change the data directly.
Pick a factory function, a plain function that builds and returns an object, when you only need to turn some input into an object once. Not every object needs a class.
Watch out when you send class objects to a server. JSON.stringify(object) turns an object into text. It keeps only the object's own data, not methods from the prototype. When the data comes back, rebuild the object with new or a factory to get the methods back.
class User { constructor(name) { this.name = name; } greet() { return "Hi " + this.name; }} const user = new User("Asha");console.log(JSON.stringify(user));console.log(typeof user.greet);Line 11 prints {"name":"Asha"}, because name is the object's own data. Line 12 prints function, so greet still works. But greet is not in the JSON text, because it is stored on the prototype.
You never have to use classes. Plain objects and factory functions are fine too. Pick a class when new makes it clear that many objects share one set of methods.
Common misconceptions
“Classes replace prototypes.”
No. Classes use prototypes. Methods, getters, and setters are still stored on ClassName.prototype.
“The constructor makes the methods for each object.”
No. The constructor sets up each object's data. Methods are written once and shared by all instances.
“Fields and methods are the same thing.”
No. Each instance gets its own fields. Methods sit on the prototype, unless you put a function in a field on purpose.
“Getters store data.”
No. A getter runs code each time you read it. It can work out a value from stored data without storing anything itself.
“Constructors can wait for data.”
No. A constructor gives back the new object right away. When setup must wait, use a static async factory.
“You can use a class above the line that makes it.”
No. That works for function declarations, but not for classes. Until the class line runs, the class name is in the temporal dead zone, and using it throws a ReferenceError.
“Static methods can read an instance's data.”
No. When you call a static method on the class, this is the class, not an instance. Pass in the data it needs, or use a normal method.
“A getter is a stored field.”
No. A getter is stored on the prototype and runs again on every read. Use a field when you need a value you can store and change.
“A class expression is a different kind of class.”
No. It follows the same rules. The only difference is that it is written as a value and stored in a variable.
Practice exercises
5 EXERCISESType the class from the starter code. Then guess the two lines it prints.
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 the object's data. The complete() method is shared, but it changes this one object through this. So it prints Ship lesson done, then true.
Guess both true or false values 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, then true. There is only one increment function, on the prototype. And both count fields are still 0.
This code throws an error. Fix the line that makes the widget, so the program prints its 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);A class must be called with new. new Widget("Panel") makes the object first. Then the constructor stores name on it.
Add a getter that turns Celsius into 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 runs the getter. It turns 20°C into 68°F.
Use a static async factory to make 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 finishes its work, then returns a normal Profile object. .then gets that object, and it prints A-avatar.
Quiz: check your understanding
8 QUESTIONSQuestion 1 of 8What is a JavaScript class?
Choose an answer to see the explanation.
Question 2 of 8What does this code print? (It checks if two objects share a method.)
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 8In a class that doesn't extend another class, what runs before the constructor body?
Choose an answer to see the explanation.
Question 4 of 8What 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 8Which statement about classes is true?
Choose an answer to see the explanation.
Question 6 of 8Why use
static async create()?Choose an answer to see the explanation.
Question 7 of 8What does this code with static members print?
Read the code, then predictclass Cart { static taxRate = 0.05; static withTax(total) { return total * (1 + Cart.taxRate); } } console.log(Cart.withTax(20));Choose an answer to see the explanation.
Question 8 of 8What does this code print? (It compares two arrow function fields.)
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 a plan for making objects that share the same methods.
- The constructor runs once for each
new. Fields run just before the constructor body. - Methods, getters, and setters are stored once, on the prototype. Fields are stored on each instance.
- Static members are stored on the class itself, so you call them on the class name.
- A class is a function value, but you must call it with
new, and only after the line that makes it. - Use
static async create()when setup must wait before the object is ready.
Remember the one-liner.
A class is a blueprint: its constructor sets up each new object, and its methods are shared by all of them.
Up next: Inheritance with extends & super.