JavaScript Intermediate #1 Classes
The first post in the JavaScript Intermediate series. After finishing the 7-part Basics series you can confidently build small tools and scripts — Intermediate is where you layer on modern JavaScript’s expressiveness.
A 7-post series.
- #1 Classes ← this post
- #2 Async intro — Promise, async/await
- #3 Iterators and generators
- #4 Destructuring/spread/rest in depth
- #5 Optional chaining and nullish coalescing
- #6 fetch API and error handling
- #7 Working with JSON and serialization
This post covers JavaScript’s class syntax from start to finish.
Class basics #
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`hi, ${this.name} (${this.age})`);
}
}
const u = new User('Curtis', 30);
u.greet(); // hi, Curtis (30)
It starts with the class keyword, and constructor is the initializer that runs when an instance is created. Inside, this refers to the new instance.
Calling without new throws an error — classes are always used with new.
User('Curtis', 30); // ✗ TypeError
new User('Curtis', 30); // OK
Methods and this
#
Inside a method, this defaults to the calling instance. But detaching the function can lose this.
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`hi, ${this.name}`);
}
}
const u = new User('Curtis');
u.greet(); // hi, Curtis
const fn = u.greet; // detached
fn(); // hi, undefined ← this gone
This is the most common pitfall with JavaScript classes. Frequent when passing a method as a callback or registering it as an event handler. Two solutions.
1) Wrap with an arrow function #
button.addEventListener('click', () => u.greet());2) bind or arrow function as a class field
#
class User {
constructor(name) {
this.name = name;
}
greet = () => {
console.log(`hi, ${this.name}`);
};
}
const u = new User('Curtis');
const fn = u.greet;
fn(); // hi, Curtis — this is preserved even when detached
You may have seen this in React callbacks. The cost — a fresh method per instance, so it’s not recommended for heavy classes.
getter / setter — methods that look like properties #
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 t = new Temperature(25);
console.log(t.fahrenheit); // 77 — accessed like a property, not a method call
t.fahrenheit = 100;
console.log(t._celsius); // ~37.7
Defined with get / set — read and write without parentheses. Fits when you want to compute on read or validate on write.
Static members — static
#
Methods/properties that belong to the class itself, not instances.
class MathUtil {
static PI = 3.14159;
static square(n) {
return n * n;
}
}
MathUtil.PI; // 3.14159
MathUtil.square(5); // 25
const m = new MathUtil();
m.square(5); // ✗ not on the instance
Fits utility-function bundles, or factory methods that help create instances (e.g., User.fromJSON(...)).
static block — ES2022
#
When static-member initialization needs complex logic, use a static { ... } block.
class Config {
static defaults = {};
static {
Config.defaults = JSON.parse(loadConfigFile());
Config.defaults.timestamp = Date.now();
}
}Not common — but you’ll meet it during library initialization.
Private fields — #
#
The official private syntax added in ES2022. External access is impossible.
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance = this.#balance + amount;
}
get balance() {
return this.#balance;
}
}
const acc = new BankAccount();
acc.deposit(100);
console.log(acc.balance); // 100
console.log(acc.#balance); // ✗ SyntaxError — external access blocked
Prefix the name with #. Not a convention — language-level enforcement, real privacy. Unlike old JavaScript’s “convention-only private (_balance),” external access is blocked.
Difference from _underscore
#
The old code’s _balance is a promise only.
class OldStyle {
constructor() {
this._balance = 0; // convention: please don't access
}
}
const o = new OldStyle();
o._balance = 999; // actually accessible
In new code, use #.
Inheritance — extends and super
#
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // call parent constructor
this.breed = breed;
}
speak() {
super.speak(); // call parent method
console.log(`${this.name} barks (woof woof)`);
}
}
const d = new Dog('Bori', 'Shiba');
d.speak();
// Bori makes a sound
// Bori barks (woof woof)
extends declares the parent class — and inside the child constructor you must call super(...) before using this. Forget it and you’ll see a ReferenceError.
super.method() calls the parent’s method, and a same-named method on the child overrides the parent’s.
Inheritance — worth using deeply? #
Inheritance is a tool that, beyond JavaScript and across OOP, breaks code fast when overused. Modern JavaScript recommends:
- Don’t introduce inheritance where a single class is enough
- Prefer composition to inheritance — have an object hold other objects
- For shared interfaces/signatures — functions and objects also suffice
React’s move from class components to function components is similar in spirit. Cases where classes are essential are rarer than they seem.
instanceof — class check
#
const d = new Dog('Bori', 'Shiba');
d instanceof Dog; // true
d instanceof Animal; // true (parent is also true)
d instanceof Object; // true
d instanceof User; // false
Walks the inheritance chain. Often used to check whether something is an instance of a particular class.
Classes vs object literals #
Guideline for choosing between a class and a plain object.
| Case | Better choice |
|---|---|
| Many instances of the same shape | class |
| Behavior (methods) tightly bound to data | class |
| Single instance, settings object, etc. | object literal |
| Functional transformations only | functions + objects |
| Hierarchy deep enough to need inheritance | class (or reconsider via composition) |
At first you’ll often find that not using classes is fine. Reach for classes only when one really fits.
Wrap-up #
What we covered:
classandnew, initializethisinsideconstructor- Detached methods lose
this— arrow function or field arrow - Method-like properties via
get/set staticfor class-level members,static {}blocks (ES2022)- True private with
#field(_underscoreis convention only) - Inheritance with
extends, callsuper(...)first - Beware overusing inheritance — modern convention is composition first
- Where classes fit vs object literals
In the next post (#2 Async Intro) we cover one of JavaScript’s most distinctive features — async — starting with Promises and async/await from scratch.