Free lesson · JavaScript Basic to Advance
Class Methods, Constructors, and Fields
The Anatomy of a Class, In Depth
Topic 8.2 introduced classes at a high level. This Topic gets into the details — what goes inside a constructor, how methods are defined and shared, what instance fields really are, and the differences between methods that look similar.
class Account {
// Instance field — initialized on every instance
type = "savings";
// Constructor — runs on 'new Account(...)'
constructor(holder, initialBalance = 0) {
this.holder = holder;
this.balance = initialBalance;
this.transactions = [];
}
// Instance method — shared via prototype
deposit(amount) {
this.balance += amount;
this.transactions.push({ type: "deposit", amount });
return this.balance;
}
withdraw(amount) {
if (amount > this.balance) return null;
this.balance -= amount;
this.transactions.push({ type: "withdraw", amount });
return this.balance;
}
// Getter — computed property
get summary() {
return `${this.holder}: ${this.balance} (${this.transactions.length} transactions)`;
}
}
const account = new Account("Olena", 100);
account.deposit(50);
account.withdraw(30);
console.log(account.summary);
console.log(account.type);
console.log(account.transactions);
This Topic walks through each piece — constructor, instance methods, instance fields, getters and setters — and the underlying mechanics for each.
The Constructor
The constructor is a special method that runs when new ClassName(...) is called. Its job is to initialize the instance — set up its initial properties from the arguments passed to new.
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const olena = new User("Olena", 35);
console.log(olena.name);
console.log(olena.age);
Inside the constructor, this refers to the newly created instance. Assigning to this.something creates own properties on the instance. Each new User gets its own name and age because the constructor runs separately for each call to new.
Three things to know:
Every class has a constructor, even if you don't write one. If you omit constructor, JavaScript provides an empty default:
class Empty { }
const e = new Empty();
console.log(e); // {} — empty instance
You can only have one constructor. Unlike some other languages, JavaScript doesn't support constructor overloading. If you need to handle different argument patterns, use default parameters or branch inside one constructor.
Constructors can return a different object. If you explicitly return an object from a constructor, that object becomes the result of new, instead of the auto-created this. This is occasionally used in advanced patterns but mostly avoided. If you return a primitive, it's ignored — the auto-created this is still returned.
class Weird {
constructor() {
return { customReturn: true }; // overrides the default 'this'
}
}
const w = new Weird();
console.log(w); // { customReturn: true }
console.log(w instanceof Weird); // false — not actually a Weird instance
This is rarely what you want, but it's worth knowing about because it sometimes appears in test code or factory patterns.
Interactive animation: A constructor with one job - validate the arguments at the door, then assign them to this.
Instance Methods
Methods defined inside a class are placed on the class's prototype. Every instance gets access to them through the prototype chain — there's only one copy of each method, shared across all instances.
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hi, I'm ${this.name}`;
}
}
const olena = new User("Olena");
const nikhil = new User("Nikhil");
// All methods come from the prototype — single shared copy
console.log(olena.greet === nikhil.greet); // true
console.log(olena.greet === User.prototype.greet); // true
console.log(Object.getPrototypeOf(olena) === User.prototype); // true
This is exactly the prototypal inheritance pattern from Topic 7.6 — methods live on the prototype, instances share them. The class syntax just hides the assignment.
Methods inside a class don't use the function keyword and aren't separated by commas:
class User {
// ✅ Correct
greet() {
return `Hi`;
}
// ✅ Correct
farewell() {
return `Bye`;
}
}
// ❌ Wrong — no 'function' keyword
// class Broken1 { function greet() {} }
// ❌ Wrong — no commas between methods
// class Broken2 { greet() {}, farewell() {} }
This is a common syntax confusion for people coming from object literals, which do use commas. Class bodies look like objects but follow different syntax rules.
Instance Fields
Instance fields are properties initialized outside the constructor. They were added to the JavaScript spec relatively recently (ES2022 standardized them) but are now widely supported.
class Counter {
count = 0; // instance field — every counter starts with count: 0
step = 1; // every counter has step: 1 by default
increment() {
this.count += this.step;
return this.count;
}
}
const c1 = new Counter();
const c2 = new Counter();
c1.step = 5; // override the default for c1
console.log(c1.increment()); // 5
console.log(c2.increment()); // 1 — c2's step is still the default
Instance fields are syntactic sugar for assignment inside the constructor. The class above is equivalent to:
class CounterEquivalent {
constructor() {
this.count = 0;
this.step = 1;
}
increment() {
this.count += this.step;
return this.count;
}
}
const c = new CounterEquivalent();
console.log(c.increment());
Both versions behave identically. The instance-field version is cleaner — fields and methods are visually distinct, and you can see at a glance what state each instance starts with.
Note: instance fields are owned by each instance, not shared. This is different from methods, which are shared via prototype. Each new instance gets its own copy of every instance field.
graph TD
A[class definition] --> B[Methods<br/>shared on prototype]
A --> C[Instance fields<br/>own to each instance]
D[instance 1] --> B
D --> E[own count, own step]
F[instance 2] --> B
F --> G[own count, own step]
style B fill:#7c3aed,color:#fff
style E fill:#10b981,color:#fff
style G fill:#10b981,color:#fff
This diagram shows the distinction between methods and instance fields in a class. Methods are shared across all instances through the prototype — there's only one copy of each method. Instance fields are own properties of each instance — every new instance gets its own copy of every field, with the default value initialized when the instance is created.
Getters and Setters in Classes
Class bodies support the same get/set syntax for accessor properties that object literals do (Topic 7.3):
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
get celsius() {
return this._celsius;
}
set celsius(value) {
if (typeof value !== "number") {
throw new Error("Temperature must be a number");
}
this._celsius = value;
}
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value) {
this._celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature(20);
console.log(temp.celsius); // 20 — looks like a property
console.log(temp.fahrenheit); // 68 — computed
temp.fahrenheit = 100;
console.log(temp.celsius); // 37.77... — derived from the new fahrenheit
This is useful for:
- Computed properties —
fahrenheitderived from_celsius - Validation — the setter checks the value before storing
- Backward compatibility — exposing legacy property names while using new internal storage
The convention with the underscore prefix (_celsius) is a signal that the property is "internal" — not for direct access. This is a convention, not enforcement. JavaScript has real private fields (covered in Topic 8.5) that do enforce privacy.
Interactive animation: A getter derives fahrenheit from one stored value, and a setter guards writes by rejecting an impossible temperature.
Method Binding Gotchas
A common source of bugs: methods of class instances can lose their this binding when detached, just like methods on regular objects.
class Counter {
count = 0;
increment() {
this.count++;
return this.count;
}
}
const c = new Counter();
console.log(c.increment()); // 1 — works normally
const detached = c.increment;
try {
console.log(detached()); // throws — this is undefined in strict mode
} catch (e) {
console.log(`Error: ${e.message}`);
}
When a class method is stored in a variable and called standalone, this is undefined (class bodies run in strict mode). This is the same detachment problem from Topic 8.1.
The standard solutions:
class Counter {
count = 0;
// Option 1: Bind in the constructor
constructor() {
this.incrementBound = this.increment.bind(this);
}
increment() {
this.count++;
return this.count;
}
// Option 2: Use a field with an arrow function
// This creates an instance-bound arrow function for this method
decrement = () => {
this.count--;
return this.count;
};
}
const c = new Counter();
const inc = c.incrementBound;
const dec = c.decrement;
console.log(inc()); // works
console.log(dec()); // works — arrow function captured 'this' from class scope
Both options work. The arrow-function-as-field pattern has become popular, especially in React components where event handlers need stable this bindings. The trade-off: each instance gets its own copy of the arrow function (not shared via prototype), so it costs more memory.
Interactive animation: A shared prototype method against an arrow field copied per instance - less memory versus a this that cannot be lost.
Common Pitfalls
Warning: The class anatomy traps.
Method commas. Class body methods are not separated by commas. Object literal properties are. Different syntax rules. Don't carry the comma habit from objects.
Confusing instance fields with methods. Fields create per-instance copies. Methods are shared via prototype. For state, use fields. For behavior, use methods.
Losing
thisin callbacks. Class methods called as callbacks lose theirthis. Bind in the constructor or use arrow function fields. Don't passinstance.methoddirectly whenthismatters.
Why This Matters in Interviews
Interview: "What's the difference between an instance method and an instance field?" is the kind of question that probes whether you understand the class/prototype mechanism. The answer: instance methods are defined on the prototype and shared; instance fields are own properties of each instance.
"Why might you write methods as arrow function fields instead of regular methods?" The answer involves
thisbinding for callbacks — arrow function fields preservethis, regular methods can lose it.Topic 8.7 — Interview Lens: this, Classes, and OO Patterns — covers the deeper interview treatment.
Try It
Exercise: Build an
Inventoryclass with the following:
- An instance field
itemsinitialized to an empty array- An instance field
taxRateinitialized to0.08- A constructor that takes an optional store name (default:
"Store")- Methods:
addItem(name, price),removeItem(name),subtotal(),total()(subtotal plus tax)- A getter
itemCountthat returnsitems.length- A getter
summarythat returns a formatted string with store name, item count, and totalThen create two inventories with different store names and verify they share methods but have independent state.
class Inventory { // your code } const a = new Inventory("AlgoInsight Store"); const b = new Inventory("AlgoInsight Books"); a.addItem("JS Course", 99); a.addItem("Coffee Mug", 15); console.log(a.summary); console.log(b.summary); console.log(a.addItem === b.addItem); // true — shared methodBy the time this is working cleanly, you've internalized all the major class mechanics — fields, methods, constructor, getters. The next Topics build on this foundation.