JavaScript · Classes & OOP

JavaScript Static & Private Class Members — Fields, Methods, and True Encapsulation

6 min read Updated 2026-06-18

Practice Static & Private Members interview questions

Two orthogonal ideas: static and private

Class members can vary along two independent axes. The first is static vs instance: does the member belong to the class itself or to each instance? The second is public vs private: can code outside the class see it? Modern JavaScript supports all combinations, including genuinely private members that the language enforces — a big upgrade over the old underscore-prefix convention. This guide covers both axes and how they combine.

Static members belong to the class

A static member lives on the class itself, not on instances. You access it through the class name. Static methods are typically utilities or factories that don't need a specific instance.

class MathUtil {
  static PI = 3.14159          // static field
  static square(x) { return x * x }   // static method
}

MathUtil.PI            // 3.14159
MathUtil.square(5)     // 25
new MathUtil().square  // undefined — not on instances

Static members are perfect for constants, helper functions, and factory methods that construct instances in controlled ways.

The factory-method pattern

Because this inside a static method refers to the class it was called on, static factories compose beautifully with inheritance.

class Model {
  constructor(data) { this.data = data }
  static fromJSON(json) { return new this(JSON.parse(json)) }  // `this` = calling class
}
class User extends Model {}

User.fromJSON('{"name":"Ada"}') instanceof User   // true

new this(...) builds a User when called as User.fromJSON, because this is User, not Model. This is a common, powerful pattern for ORMs and value objects.

Accessing instances from static methods

A frequent use of static methods is operating across instances — comparisons, aggregations, or counting.

class Point {
  constructor(x, y) { this.x = x; this.y = y }
  static distance(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y)   // takes two instances
  }
}

Point.distance(new Point(0, 0), new Point(3, 4))   // 5

This reads naturally as a class-level operation rather than awkwardly living on one instance.

Private fields with #

The # prefix declares a truly private field — enforced by the language, not by convention. Private members are inaccessible from outside the class body; attempting to read one is a syntax error.

class BankAccount {
  #balance = 0                  // private field

  deposit(amount) { this.#balance += amount; return this.#balance }
  getBalance() { return this.#balance }
}

const acc = new BankAccount()
acc.deposit(100)     // 100
acc.getBalance()     // 100
acc.#balance         // SyntaxError — private, even at runtime

Unlike the old _balance convention (which anyone could read or write), #balance is genuinely hidden. It won't appear in Object.keys, JSON.stringify, or for...in, and it can't be accessed via bracket notation either.

Private methods and accessors

The # syntax extends to methods and getters/setters, letting you hide internal helpers.

class Temperature {
  #celsius = 0
  #validate(c) {                 // private method
    if (c < -273.15) throw new RangeError('below absolute zero')
    return c
  }
  set value(c) { this.#celsius = this.#validate(c) }
  get value() { return this.#celsius }
}

Private methods keep your public API clean — consumers see only value, while #validate stays an internal implementation detail you're free to change.

Checking for a private field — the in trick

You can test whether an object has a particular private field using #field in obj. This is the idiomatic "brand check" to confirm an object is a real instance of your class.

class Stack {
  #items = []
  static isStack(obj) {
    return #items in obj   // true only for genuine Stack instances
  }
}

Stack.isStack(new Stack())   // true
Stack.isStack({})            // false

This is more robust than instanceof for confirming an object actually went through your constructor, since private fields can only be installed by the class itself.

Static private members

You can combine both axes: static private fields and methods belong to the class and are hidden from outside. They're ideal for shared internal state like caches or instance registries.

class IdGenerator {
  static #counter = 0           // static private field
  static #next() { return ++IdGenerator.#counter }  // static private method

  static create() { return { id: IdGenerator.#next() } }
}

IdGenerator.create()   // { id: 1 }
IdGenerator.create()   // { id: 2 }
IdGenerator.#counter   // SyntaxError — private

The counter is encapsulated entirely within the class; no external code can read or tamper with it.

Static initialization blocks

Sometimes static setup needs more than a single expression — multiple statements, try/catch, or access to private static fields. A static block (static { ... }) runs once when the class is defined.

class Config {
  static #settings = {}
  static {
    const raw = loadConfigFile()
    for (const [k, v] of Object.entries(raw)) {
      Config.#settings[k] = v   // can touch private statics during setup
    }
  }
  static get(key) { return Config.#settings[key] }
}

Static blocks run in source order alongside static field initializers, giving you a clean place for complex class-level initialization.

Encapsulation: enforced vs convention

Before #, the community used a leading underscore (_balance) to signal "please don't touch." It was a gentleman's agreement — nothing stopped access.

class Old {
  constructor() { this._secret = 42 }   // convention only fully accessible
}
new Old()._secret   // 42 — anyone can read/write it

True # privacy changes the contract: the language guarantees outsiders can't reach in. This matters for libraries (consumers can't depend on internals) and for invariants (state can't be corrupted from outside). Prefer # for anything that should be genuinely internal; reserve the underscore convention for legacy code or quick scripts.

Key takeaways

  • Static members live on the class, not instances — great for constants, utilities, and factory methods where this is the calling class.
  • Private members use the # prefix and are enforced by the language: invisible to enumeration, serialization, and external code.
  • Private methods and accessors keep the public API minimal while hiding implementation.
  • #field in obj is a reliable brand check that an object is a true instance.
  • Static private members encapsulate shared class state like counters and caches; static blocks handle complex one-time setup.
  • # provides real encapsulation, unlike the old _underscore convention which was only a hint.

Together, static and private members let you design classes with a clean public surface and a protected internal core — the foundation of robust object-oriented JavaScript.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.