JavaScript · Classes & OOP

JavaScript Class Syntax & Methods Explained — Fields, Getters, and the Prototype Truth

6 min read Updated 2026-06-18

Practice Class Syntax & Methods interview questions

Classes are sugar — but useful sugar

JavaScript's class keyword, added in ES2015, gives an object-oriented syntax that feels familiar to developers from Java, C#, or Python. But it's crucial to understand the headline fact up front: classes are syntactic sugar over constructor functions and prototypes. There's no new object model underneath — class just makes the existing prototype machinery cleaner and less error-prone to write. Knowing this keeps you grounded: every class feature maps to something you could write by hand with functions and prototype.

That said, the sugar is genuinely valuable. It standardizes patterns, adds real encapsulation (private fields), and eliminates entire categories of bugs from the old manual-wiring days.

Declaring a class

A class declaration bundles a constructor and methods into one block.

class Circle {
  constructor(radius) {
    this.radius = radius          // instance field set in constructor
  }
  area() {
    return Math.PI * this.radius ** 2
  }
}

const c = new Circle(5)
c.area()   // 78.54...

The constructor runs when you call new Circle(5) and initializes instance state. The area method is not copied onto each instance — it lives on Circle.prototype, exactly as if you'd written Circle.prototype.area = function () {...}. You can verify it:

Object.getPrototypeOf(c) === Circle.prototype   // true
c.hasOwnProperty('area')                         // false — it's on the prototype

Class declarations vs expressions

Like functions, classes come in declaration and expression forms.

class A {}                       // declaration
const B = class {}               // anonymous class expression
const C = class Named {}         // named class expression

One important difference from function declarations: classes are not hoisted in a usable way. They live in a "temporal dead zone" until the declaration is evaluated, so you can't use a class before you define it.

new Foo()              // ReferenceError — TDZ
class Foo {}

This stricter behavior is intentional — it prevents the confusing "use before define" that function hoisting allows.

Instance fields

Beyond setting fields in the constructor, you can declare class fields directly in the class body. They're initialized on each instance before the constructor body runs.

class Counter {
  count = 0           // instance field with a default
  step = 1

  increment() {
    this.count += this.step
    return this.count
  }
}

new Counter().increment()   // 1

Field declarations make defaults explicit and readable, and they're set per-instance (unlike methods, which are shared). A subtle point: a field initializer that references this runs in field-declaration order, so later fields can build on earlier ones.

Methods, getters, and setters

Methods defined in the class body land on the prototype. You can also define accessor properties with get and set, which look like fields but run functions.

class Temperature {
  constructor(celsius) { this.celsius = celsius }

  get fahrenheit() { return this.celsius * 9 / 5 + 32 }
  set fahrenheit(f) { this.celsius = (f - 32) * 5 / 9 }
}

const t = new Temperature(20)
t.fahrenheit          // 68   — getter
t.fahrenheit = 212    // setter
t.celsius             // 100

Getters and setters are ideal for derived values and validation. Like methods, they live on the prototype, so all instances share the accessor functions.

Computed method names

Method names can be computed with brackets, just like object literal keys — useful for dynamic or symbol-based methods.

const sym = Symbol('run')
const methodName = 'go'

class Machine {
  [methodName]() { return 'going' }      // computed string name
  [sym]() { return 'running' }           // symbol-keyed method
  *[Symbol.iterator]() { yield 1; yield 2 }  // makes instances iterable
}

Defining [Symbol.iterator] as a generator method is the idiomatic way to make a class's instances work with for...of and spread.

this inside methods

A method's this is the instance it was called on — but only if you call it as a method. Detach it and this is lost, the same gotcha as with any function.

class Button {
  constructor(label) { this.label = label }
  click() { return `${this.label} clicked` }
}

const b = new Button('OK')
const fn = b.click
fn()   // TypeError — `this` is undefined (class bodies are strict mode)

Two common fixes: bind in the constructor (this.click = this.click.bind(this)) or use a field with an arrow function, which captures this lexically.

class Button {
  constructor(label) { this.label = label }
  click = () => `${this.label} clicked`   // arrow field — `this` always bound
}
const b = new Button('OK')
const fn = b.click
fn()   // 'OK clicked' — works detached

The tradeoff: arrow-function fields are created per instance (not shared on the prototype), costing a bit more memory. For event handlers that get passed around, that's usually a worthwhile trade.

Strict mode by default

Everything inside a class body runs in strict mode automatically, regardless of the surrounding code. That's why detached methods get undefined for this instead of the global object, and why silent failures (like writing to a read-only property) become loud errors. This stricter default catches bugs early.

Classes must be called with new

Unlike old constructor functions, a class throws if you forget new.

class User { constructor(name) { this.name = name } }
User('Ada')   // TypeError: Class constructor User cannot be invoked without 'new'

This is a feature: it eliminates the silent forgotten-new bug that plagued constructor functions. There's no way to accidentally pollute the global object with a class.

What it compiles to

To cement the "sugar" point, here's the rough equivalent of a simple class written the old way:

// class Circle { constructor(r){ this.r = r } area(){ return Math.PI*this.r**2 } }
function Circle(r) {
  'use strict'
  this.r = r
}
Circle.prototype.area = function () { return Math.PI * this.r ** 2 }

The class version adds the must-use-new guard, strict mode, and cleaner syntax — but the prototype-based result is identical. Whenever class behavior puzzles you, mentally translate it back to constructors and prototypes and the answer usually appears.

Key takeaways

  • class is syntactic sugar over constructor functions and prototypes — no new object model.
  • Methods, getters, and setters live on the prototype and are shared; instance fields are per-instance.
  • Class bodies are always strict mode, and classes can't be used before declaration (TDZ).
  • this follows normal rules — detached methods lose it; arrow-function fields capture it lexically at a per-instance memory cost.
  • Classes throw if called without new, eliminating the forgotten-new bug.
  • Computed names and [Symbol.iterator] generator methods enable dynamic and iterable classes.

Treat class as a clean, safer wrapper over the prototype system you already understand, and its every behavior becomes predictable rather than magical.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.