Topic 7.5
Classes
In one line
A class is a cleaner way to write a constructor and its prototype methods in one block, with fields, static members, getters and setters; under the hood it builds the same prototype chain, but adds safety rules such as strict mode and refusing to run without new.
Think of it like this
A form you fill in at a bank. The blank form is the class: it lists the boxes every account has (name, balance) and the rules ("deposits must be positive"). Each filled-in form is an instance: same boxes, different answers. A note printed at the top of the form, like "Branch opened in 1998", belongs to the form itself, not to any customer: that's a static member.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Class
- A blueprint for objects, written with
class Name { ... }. It is a special kind of function. - constructor
- The method in a class that runs once for every new instance, to set it up.
- Public field
- A property declared in the class body (
count = 0;) that every instance gets its own copy of. - Static member
- A property or method on the class itself (
Counter.created), not on instances. - Factory method
- A static method that creates and returns an instance in a special way, like
Temperature.fromFahrenheit(32). - Getter / setter
- Methods marked
getorsetthat run when a property is read or assigned. - Syntax sugar
- A nicer way to write something the language could already do.
- Enumerable
- Shows up when looping over keys (
for...in,Object.keys). Class methods are not enumerable.
Step by step
01Declare a class
The class name starts with a capital. The constructor receives the arguments from new and stores them on this.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const a = new Point(3, 4); // Point { x: 3, y: 4 }02Add methods and a getter
Methods go straight in the body, without the word function and without commas. A getter looks like a property from outside.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distanceTo(other) {
return Math.hypot(other.x - this.x, other.y - this.y);
}
get length() {
return Math.hypot(this.x, this.y);
}
}
new Point(3, 4).length; // 5 (no brackets: it is a getter)03Add fields and static members
count = 0; gives each counter its own starting value. static created = 0; is one number shared by the class, read as Counter.created.
class Counter {
count = 0;
static created = 0;
constructor() {
Counter.created++;
}
increment() {
this.count++;
}
}04See what the class built
The class is the constructor function. Methods and getters are on its prototype; fields are on each instance; static members are on the class itself.
05Know the extra rules
A class can't be called without new, can't be used before its declaration line, and its code is always strict. These rules turn silent bugs from Topic 7.4 into clear errors.
Try it yourself
- 1
Add a static factory
In the first example, add
static fromArray([x, y]) { return new Point(x, y); }and logPoint.fromArray([6, 8]).length. Predict the number first. - 2
Swap a field for a method
In the last example, change
handleClick = () => { ... };into a normal methodhandleClick() { ... }(keep the body). Predict what happens on the first click, then check the console. - 3
Make a field depend on another
In the second example, add a field
label = "Counter #" + Counter.created;above the constructor. Predictc2.label. (Fields run before the constructor body.)
Code & diagrams
`length` is read without brackets because it is a getter. Joining the point with a string calls `toString()` automatically.
Preview
Console
Nothing logged yet.Expected console output
Point { x: 3, y: 4 }
5 5 (3, 4)
function [class Point]Fields appear on each instance in the order they were declared. Static members live on the class, so instances can't see them.
Preview
Console
Nothing logged yet.Expected console output
Counter { count: 2, step: 1 } Counter { count: 5, step: 5 }
created: 2 | on an instance: undefined
[ 2, 5 ]Only `celsius` is stored. `fahrenheit` is computed by the getter and converted back by the setter, which also checks the value.
Preview
Console
Nothing logged yet.Expected console output
77
100 Temperature { celsius: 100 }
Temperature { celsius: 0 }
TypeError: fahrenheit must be a numberThe method `show` is on the prototype but hidden from `for...in`. The last line shows a class refusing a call without `new`.
Preview
Console
Nothing logged yet.Expected console output
true
[ 'constructor', 'show' ]
enumerable keys on the prototype: []
for...in sees x
Uncaught TypeError: Class constructor Point cannot be invoked without 'new'The arrow is created per instance (a field), so its `this` is that instance. The cost: each instance has its own copy instead of one shared prototype method.
Preview
Console
Nothing logged yet.Expected console output
on: true
on: false
own copy per instance? trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a class before its declaration
Write const p = new Point(1, 2); above class Point { ... }.
Break #2
Put commas between methods
Write class A { one() {}, two() {} } (with a comma, as in an object literal).
Break #3
Pass a class method as a callback
Inside a class, write button.addEventListener("click", this.save); where save() { this.saved = true; } is a normal method.
Myth vs fact
Myth
JavaScript classes work like Java or C# classes.
Fact
They are a syntax over prototypes: instances link to a prototype object at runtime, and you can still change prototypes, add methods later, or create instances with Object.create. There are no interfaces, overloads or compile-time types.
Myth
Class methods are automatically bound to the instance.
Fact
They are ordinary prototype methods with dynamic this. Only arrow-function fields are bound per instance.
Myth
A field like count = 0; lives on the prototype.
Fact
Fields are created on each instance (they show in Object.keys and JSON). Only methods, getters and setters are on the prototype.
Myth
Classes are always better than plain objects and functions.
Fact
They shine when you have many instances sharing behaviour. For one-off objects or pure data, a literal or a few functions is simpler (Topic 7.8).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Fields are defined with **
[[DefineOwnProperty]]** semantics, not assignment: a field namedxin a subclass replaces an inherited accessorxinstead of calling its setter. TypeScript's olduseDefineForClassFields: falseused assignment, which is why switching it on can change behaviour. - ▸
Arrow-function fields cost one closure per instance and can't be overridden through
super.method()or spied on via the prototype in tests. For classes with many instances, prefer prototype methods and bind only where a callback is needed. - ▸
Class bodies can use computed names (
[Symbol.iterator]() { ... },["get" + name]() {}), generator methods (*items() {}) and async methods. Implementing[Symbol.iterator]makes instances work withfor...ofand spread. - ▸
A class has an inner binding of its own name that can't be reassigned inside the class body, and
class X {}at the top level of a script does not createwindow.X(likelet). Browser features such as custom elements (class MyCard extends HTMLElement) require real classes, andextendsis covered in Topic 7.7.
Remember this
- 1
A class (ES2015) groups everything about one kind of object in one place.
class Point { ... }declares it, andnew Point(3, 4)creates an instance. The special method **constructor(x, y)** runs once for each new instance, withthisset to that instance, exactly like a constructor function (Topic 7.4). If you leave it out, an empty one is used. - 2
Methods written in the class body, like
toString() { ... }, are put onPoint.prototype, so all instances share them through the prototype chain (Topic 7.3). Unlike properties you add by hand, class methods are non-enumerable: they don't show infor...inorObject.keys. Methods are separated by nothing: no commas, unlike an object literal. - 3
Public fields (ES2022) declare instance properties right in the body:
count = 0;orstep;(starts asundefined). Each instance gets its own copy, created just before the constructor body runs (in a base class), in the order written. A field can usethisand earlier fields:total = this.price * 2;. Fields are good for defaults; the constructor is for values that come from arguments. - 4
**
staticmembers belong to the class itself**, not to instances:static created = 0;andstatic compare(a, b) { ... }are used asCounter.createdandCounter.compare(...). Use them for helpers and factory methods (alternative ways to create an instance, likeTemperature.fromFahrenheit(32)), and for counters or settings shared by the whole class.static { ... }blocks (ES2022) run setup code once, when the class is defined. - 5
Getters and setters (
get fahrenheit() { ... },set fahrenheit(f) { ... }, from Topic 6.7) look like properties from outside but run code:t.fahrenheitcalls the getter,t.fahrenheit = 212calls the setter. Use them for values computed from other fields and for checking a value before storing it. Like methods, they live on the prototype. - 6
A class is mostly syntax sugar (a nicer way to write the same thing) over constructor functions:
typeof Pointis"function", and instances are linked toPoint.prototype. But it adds real rules: the body is always in strict mode; callingPoint()withoutnewthrows; methods can't be used withnew; and a class declaration is not usable before its line (it is in the temporal dead zone, Topic 3.5, unlike afunctiondeclaration). Classes can also be written as expressions:const Point = class { ... };.
Explain it without notes
What does the class syntax actually create? Is it only syntax sugar?
Where do constructor properties, fields, methods, getters and static members end up?
When would you use a static method instead of an instance method?
Why might you write a method as an arrow-function field, and what does it cost?
Practice
Write a class Rectangle with width and height from the constructor, a getter area, a method scale(factor) that returns a new Rectangle, and a static square(size). Print new Rectangle(2, 3).area and Rectangle.square(4).scale(2).area.
Write a class Ticket that gives every new ticket an id from a static counter (1, 2, 3, ...). Create three tickets and print their ids.
Write a class Username with a setter value that trims the text and throws a RangeError if it's shorter than 3 characters, and a getter that returns it. Show a valid and an invalid assignment.
Trade-offs
- ↔
Classes give a familiar, organised structure and safety rules, but encourage putting everything into objects with
this, which brings back the lost-thisproblems; plain functions avoidthisbut don't share methods automatically. - ↔
Arrow-function fields make callbacks safe, but cost memory per instance and can't be overridden through
super; prototype methods are cheaper but needbindor a wrapper when passed around.
Done when you can
I can write a class with a constructor, methods, a getter and a setter.
I can use public fields and static members and say where each one lives.
I can explain how a class maps onto a constructor function and its prototype.
I know the extra rules: strict mode,
newrequired, TDZ, no commas.I can keep
thisfor callbacks with an arrow field orbind.