Class Declaration vs Class Expression
You can define a class in two ways: a class declaration uses the class keyword followed by a name, while a class expression assigns the class to a variable. Both create the same result.
// Class declaration
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name} and I'm ${this.age} years old.`;
}
}
// Class expression (anonymous)
const Animal = class {
constructor(species) {
this.species = species;
}
};
// Class expression (named – name is only visible inside the class body)
const Vehicle = class VehicleClass {
constructor(make) {
this.make = make;
}
};
const p = new Person('Alice', 30);
console.log(p.greet());
const dog = new Animal('Canis lupus familiaris');
console.log(dog.species);
Hi, I'm Alice and I'm 30 years old. Canis lupus familiaris
Unlike function declarations, class declarations are not hoisted. Trying to use a class before it is declared throws a ReferenceError. Always declare your classes before using them.
The constructor() Method
The constructor is a special method that runs automatically when you use new. It initialises instance properties and can accept any number of parameters. A class can only have one constructor; having more than one throws a SyntaxError.
class Rectangle {
constructor(width, height) {
// Validate inputs inside the constructor
if (width <= 0 || height <= 0) {
throw new Error('Dimensions must be positive numbers');
}
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
perimeter() {
return 2 * (this.width + this.height);
}
toString() {
return `Rectangle(${this.width} × ${this.height})`;
}
}
const rect = new Rectangle(5, 3);
console.log(rect.area()); // 15
console.log(rect.perimeter()); // 16
console.log(rect.toString()); // Rectangle(5 × 3)
console.log(typeof rect); // object
// What typeof tells you about a class itself:
console.log(typeof Rectangle); // function (classes ARE functions under the hood)
15 16 Rectangle(5 × 3) object function
Public Class Fields
Class fields let you declare and initialise instance properties directly in the class body — without putting them in the constructor. They run before the constructor body and make the shape of an instance immediately obvious.
class Counter {
// Public class fields (initialised to default values)
count = 0;
step = 1;
label = 'counter';
constructor(label = 'counter', step = 1) {
this.label = label;
this.step = step;
}
increment() { this.count += this.step; }
decrement() { this.count -= this.step; }
reset() { this.count = 0; }
toString() {
return `[${this.label}] count = ${this.count}`;
}
}
const c = new Counter('clicks', 2);
c.increment();
c.increment();
c.increment();
console.log(c.toString()); // [clicks] count = 6
// Fields are own properties, not on the prototype
console.log(Object.hasOwn(c, 'count')); // true
console.log(Object.hasOwn(c, 'increment')); // false (method is on prototype)
[clicks] count = 6 true false
Static Methods and Properties
Static members belong to the class itself, not to instances. You call them on the class directly. They are commonly used for utility functions, factory methods, and shared configuration.
class MathUtils {
// Static property
static PI = 3.14159265358979;
static instanceCount = 0;
constructor() {
MathUtils.instanceCount++;
}
// Static methods – no access to 'this' (instance)
static circleArea(r) {
return MathUtils.PI * r * r;
}
static clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// Factory method – a common static pattern
static fromDegrees(degrees) {
return (degrees * MathUtils.PI) / 180;
}
}
console.log(MathUtils.circleArea(5)); // 78.539...
console.log(MathUtils.clamp(150, 0, 100)); // 100
console.log(MathUtils.fromDegrees(180)); // 3.14159...
new MathUtils();
new MathUtils();
console.log(MathUtils.instanceCount); // 2
78.53981633974483 100 3.14159265358979 2
Use static methods for operations that are logically related to the class but don't need access to instance data — things like validation helpers, factory methods, or utility calculations. If the function needs this, it should be an instance method.
Getters and Setters
Getters (get) and setters (set) let you define computed properties and add validation when reading or writing a value. They look like properties from the outside but run functions internally.
class Temperature {
#celsius = 0; // private field (covered in Encapsulation lesson)
constructor(celsius) {
this.celsius = celsius; // goes through the setter
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (typeof value !== 'number') throw new TypeError('Temperature must be a number');
if (value < -273.15) throw new RangeError('Below absolute zero!');
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9/5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9; // delegates to celsius setter
}
get kelvin() {
return this.#celsius + 273.15;
}
toString() {
return `${this.#celsius}°C / ${this.fahrenheit}°F / ${this.kelvin}K`;
}
}
const temp = new Temperature(100);
console.log(temp.toString()); // 100°C / 212°F / 373.15K
temp.fahrenheit = 32;
console.log(temp.celsius); // 0
console.log(temp.toString()); // 0°C / 32°F / 273.15K
100°C / 212°F / 373.15K 0 0°C / 32°F / 273.15K
Class vs Object Literal
| Feature | Class | Object Literal |
|---|---|---|
| Multiple instances | Yes – use new | No – single object |
| Inheritance | Yes – extends | Manual prototype wiring |
| Private fields | Yes – #field | No native support |
| Static members | Yes – static | Add directly to object |
| Getters/Setters | Yes | Yes (get/set keywords) |
| Use case | Blueprints for many objects | One-off singleton config |
typeof | "function" (class itself) | "object" |
Class vs Function Constructor
Before ES6, objects were created with function constructors and new. Classes are syntactic sugar over this pattern — they compile down to nearly the same prototype-based code.
// Pre-ES6: Function constructor pattern
function PersonOld(name, age) {
this.name = name;
this.age = age;
}
PersonOld.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
PersonOld.create = function(name, age) { // static-like
return new PersonOld(name, age);
};
// ES6+ Class – same result, cleaner syntax
class PersonNew {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name}`;
}
static create(name, age) {
return new PersonNew(name, age);
}
}
// Both produce the same prototype chain
const a = new PersonOld('Alice', 30);
const b = new PersonNew('Bob', 25);
console.log(a.greet()); // Hi, I'm Alice
console.log(b.greet()); // Hi, I'm Bob
// Key difference: classes CANNOT be called without new
// PersonNew('Eve', 20); // TypeError: Cannot call a class as a function
Practical Example – BankAccount Class
Let's bring it all together with a realistic BankAccount class that uses fields, a constructor, instance methods, a static method, and getters.
class BankAccount {
// Public class field
currency = 'USD';
// Transaction history
#transactions = [];
static #nextId = 1000;
constructor(owner, initialBalance = 0) {
this.owner = owner;
this.#transactions.push({ type: 'open', amount: initialBalance });
this.id = `ACC-${BankAccount.#nextId++}`;
}
get balance() {
return this.#transactions.reduce((sum, t) => {
return t.type === 'credit' ? sum + t.amount
: t.type === 'debit' ? sum - t.amount
: t.type === 'open' ? sum + t.amount
: sum;
}, 0);
}
deposit(amount) {
if (amount <= 0) throw new Error('Deposit must be positive');
this.#transactions.push({ type: 'credit', amount, date: new Date() });
return this;
}
withdraw(amount) {
if (amount <= 0) throw new Error('Amount must be positive');
if (amount > this.balance) throw new Error('Insufficient funds');
this.#transactions.push({ type: 'debit', amount, date: new Date() });
return this;
}
getStatement() {
return this.#transactions.map(t =>
`${t.type.padEnd(6)}: ${t.amount} ${this.currency}`
).join('\n');
}
static transfer(from, to, amount) {
from.withdraw(amount);
to.deposit(amount);
console.log(`Transferred ${amount} ${from.currency} from ${from.id} to ${to.id}`);
}
}
const alice = new BankAccount('Alice', 500);
const bob = new BankAccount('Bob', 100);
alice.deposit(200).deposit(50); // chaining
alice.withdraw(100);
BankAccount.transfer(alice, bob, 75);
console.log('Alice balance:', alice.balance); // 575
console.log('Bob balance:', bob.balance); // 175
console.log(alice.getStatement());
Transferred 75 USD from ACC-1000 to ACC-1001 Alice balance: 575 Bob balance: 175 open : 500 USD credit: 200 USD credit: 50 USD debit : 100 USD debit : 75 USD
Notice deposit and withdraw return this. This enables fluent method chaining: alice.deposit(200).deposit(50).withdraw(100). It's a common pattern in well-designed class APIs.
Exercise
Create a ShoppingCart class with:
- A
itemsarray field initialised to[] - An
addItem(name, price, qty)method - A
removeItem(name)method - A
totalgetter that returns the sum ofprice * qty - A static
merge(...carts)method that combines multiple carts
Challenge
Extend the BankAccount example above to add:
- A
transactionLimitfield capping individual withdrawals - A
freeze()/unfreeze()method pair using a private boolean - A
historygetter that returns a copy (not a reference) of the transactions array - Ensure
withdrawthrows if the account is frozen
Summary
classis syntactic sugar over prototype-based inheritance.- The
constructor()method initialises instance properties and runs whennewis called. - Public class fields declare instance properties directly in the class body.
- Static methods/properties belong to the class, not instances — call them as
ClassName.method(). - Getters (
get) and setters (set) let you compute values and validate writes. - Classes are not hoisted — declare them before use.
typeof ClassNamereturns"function"because classes are functions.
Common Interview Questions
- What is the difference between a class declaration and a class expression?
- What does
typeof MyClassreturn and why? - Are classes hoisted in JavaScript?
- What is the difference between a static method and an instance method?
- When would you use a getter instead of a regular method?
FAQ
Can a class have multiple constructors?
No. A class body can only contain one method named constructor. Attempting to define two throws a SyntaxError. To handle different argument shapes, use default parameters or conditional logic inside the single constructor.
Are class methods enumerable?
No — methods defined in a class body are non-enumerable by default, which means they won't appear in for...in loops. This is different from manually assigning to a prototype where properties are enumerable.
Can you add new methods to a class after it's defined?
Yes, by adding to ClassName.prototype: MyClass.prototype.newMethod = function() {...}. However, this is generally discouraged because it makes the code harder to follow. For production code, define all methods in the class body.
What is the difference between class fields and constructor assignments?
Class fields are always initialised first (before the constructor body runs) and are syntactically concise. Constructor assignments give you more flexibility (conditionals, parameters), but both create own properties on the instance. For simple defaults, class fields are preferred.
Do static fields get inherited?
Yes. When a class extends another, the subclass inherits static methods and properties through the prototype chain of the class objects themselves. SubClass.staticMethod() will find the method on the parent class if not overridden.