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
thisis 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 objis 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_underscoreconvention 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.