AlgoInsight

Free lesson · JavaScript Basic to Advance

Static Methods, Private Fields, Modern Class Features

Easy5 min read

Beyond Basic Classes

Modern JavaScript classes have several features beyond the basics covered in Topics 8.2-8.4. The two most important: static methods (functions attached to the class itself, not instances) and private fields (state that's truly inaccessible from outside).

class User {
  // Static field — belongs to the class
  static count = 0;

  // Private field — only accessible inside the class
  #balance = 0;

  constructor(name) {
    this.name = name;
    User.count++;
  }

  // Instance method
  deposit(amount) {
    this.#balance += amount;
  }

  getBalance() {
    return this.#balance;
  }

  // Static method — called on the class
  static getTotalUsers() {
    return User.count;
  }
}

const olena = new User("Olena");
const nikhil = new User("Nikhil");

olena.deposit(100);
console.log(olena.getBalance());     // 100
// console.log(olena.#balance);      // ❌ SyntaxError — private outside class
console.log(User.getTotalUsers());   // 2 — called on the class

This Topic covers static members, private fields, and a few additional class features you'll encounter in modern JavaScript codebases.

Interactive animation: A static factory lives on the class, an instance method on the prototype - neither is reachable from the other.

Static Methods and Fields

Static members belong to the class itself, not to instances. You call them on the class name directly, not on an instance.

class MathUtils {
  static PI = 3.14159;

  static circleArea(radius) {
    return MathUtils.PI * radius * radius;
  }

  static circumference(radius) {
    return 2 * MathUtils.PI * radius;
  }
}

console.log(MathUtils.PI);                 // 3.14159
console.log(MathUtils.circleArea(5));      // 78.53975
console.log(MathUtils.circumference(5));   // 31.4159

// You don't instantiate MathUtils — it's just a namespace
// const m = new MathUtils();              // would work, but pointless

MathUtils.PI and MathUtils.circleArea are accessed directly on the class. There's no need to create an instance. This is useful for:

Utility namespaces. Math, string formatting, or other functions that don't need per-instance state.

Factory methods. Static methods that create and return instances of the class itself:

class User {
  constructor(name, age, role) {
    this.name = name;
    this.age = age;
    this.role = role;
  }

  // Static factory methods for common cases
  static createAdmin(name) {
    return new User(name, 0, "admin");
  }

  static createGuest() {
    return new User("Guest", 0, "guest");
  }
}

const admin = User.createAdmin("Olena");
const guest = User.createGuest();

console.log(admin);
console.log(guest);

Factory methods make it clear what kind of user you're creating. The code reads better than new User("Olena", 0, "admin") because the intent is in the method name.

Counting and tracking. Like the count example at the top of this Topic, static fields can track aggregate information across all instances.

Static members are inherited through the constructor chain that extends sets up. A subclass can access the parent's static methods through itself or through super:

class Animal {
  static describe() {
    return "Some kind of animal";
  }
}

class Dog extends Animal {
  static describe() {
    return `${super.describe()}, specifically a dog`;
  }
}

console.log(Animal.describe());   // "Some kind of animal"
console.log(Dog.describe());      // "Some kind of animal, specifically a dog"

Private Fields with #

Private fields are properties that can only be accessed from inside the class body. They were added to the spec relatively recently and are now widely supported.

class Account {
  #balance = 0;             // private field, with default
  #transactions = [];

  constructor(holder) {
    this.holder = holder;
  }

  deposit(amount) {
    this.#balance += amount;
    this.#transactions.push({ type: "deposit", amount });
  }

  withdraw(amount) {
    if (amount > this.#balance) return null;
    this.#balance -= amount;
    this.#transactions.push({ type: "withdraw", amount });
    return amount;
  }

  getBalance() {
    return this.#balance;
  }

  getTransactionCount() {
    return this.#transactions.length;
  }
}

const account = new Account("Olena");
account.deposit(100);
account.withdraw(30);

console.log(account.getBalance());           // 70
console.log(account.getTransactionCount());  // 2

// console.log(account.#balance);            // ❌ SyntaxError — private
// account.#balance = 999999;                 // ❌ same

The # prefix is part of the field name. #balance is genuinely inaccessible outside the class — JavaScript enforces this at the syntax level. Trying to access account.#balance from outside is a syntax error, not just a runtime error.

This gives you true encapsulation in classes. Before private fields, JavaScript developers used:

Private fields replace all of these for class-based code. They're cleaner and they work.

Private methods follow the same pattern:

class Account {
  #balance = 0;

  deposit(amount) {
    if (!this.#isValidAmount(amount)) return false;
    this.#balance += amount;
    return true;
  }

  #isValidAmount(amount) {
    return typeof amount === "number" && amount > 0;
  }
}

const account = new Account();
account.deposit(50);   // works
// account.#isValidAmount(50);   // ❌ SyntaxError — private method

#isValidAmount is a private method. It's only accessible from inside the class body. This is useful for internal helper methods that shouldn't be part of the class's public API.

Interactive animation: Reading acc.#balance from outside the class does not even parse - it is a SyntaxError, not undefined.

Interactive animation: #private fields against closure privacy - what enforces each wall, where the methods live and what each one needs.

Checking Class Membership

Two ways to check whether something is an instance of a class:

class Animal {}
class Dog extends Animal {}

const rex = new Dog();

// instanceof — walks the prototype chain
console.log(rex instanceof Dog);     // true
console.log(rex instanceof Animal);  // true (Dog extends Animal)
console.log(rex instanceof Object);  // true (everything extends Object)

// constructor — direct check (less common)
console.log(rex.constructor === Dog);     // true
console.log(rex.constructor === Animal);  // false — instance was created with Dog

instanceof is more useful because it handles inheritance correctly. rex instanceof Animal is true because Animal.prototype is in rex's chain — even though rex was created from Dog.

constructor checks the immediate class only. This is sometimes useful when you specifically want to know "was this object made by Dog, not Animal," but it doesn't handle subclasses well.

graph LR
    A[rex] --> B[Dog.prototype]
    B --> C[Animal.prototype]
    C --> D[Object.prototype]

    E[instanceof Dog] -.->|searches chain| B
    F[instanceof Animal] -.->|searches chain| C
    G[instanceof Object] -.->|searches chain| D

    style E fill:#10b981,color:#fff
    style F fill:#10b981,color:#fff
    style G fill:#10b981,color:#fff
This diagram shows how instanceof works. An instance like rex has a chain through Dog's prototype, Animal's prototype, and Object's prototype. The instanceof operator searches this chain for the given constructor's prototype. instanceof Dog finds it on the first hop. instanceof Animal finds it on the second hop. instanceof Object finds it on the third hop. All three return true because all three prototypes are in the chain.

A Note on this in Static Methods

Inside a static method, this refers to the class itself, not to an instance. This is occasionally useful — static methods can call other static methods through this:

class Logger {
  static prefix = "[LOG]";

  static info(msg) {
    console.log(`${this.prefix} INFO: ${msg}`);
  }

  static warn(msg) {
    console.log(`${this.prefix} WARN: ${msg}`);
  }

  static error(msg) {
    this.warn(`Error happened: ${msg}`);   // call another static via this
  }
}

Logger.error("Something broke");

When Logger.error("...") runs, this is Logger. So this.warn(...) is equivalent to Logger.warn(...). This makes static methods composable.

Common Pitfalls

Warning: The modern class feature traps.

Confusing static and instance members. Static members exist on the class. Instance members exist on instances. Calling User.deposit() when deposit is an instance method throws. Calling account.getTotalUsers() when getTotalUsers is static throws.

Trying to access private fields by string. account["#balance"] doesn't work — private fields are not regular properties. The # is part of the syntax, not the name.

Forgetting that instanceof walks the chain. obj instanceof Object is true for almost everything. To check the exact class, use obj.constructor === ExactClass (but be careful with subclasses).

Why This Matters in Interviews

Interview: "What's the difference between a static method and an instance method?" tests understanding of the class structure. The answer: instance methods are on the prototype (Class.prototype.method) and called on instances; static methods are on the class itself (Class.method) and don't need an instance.

"How do you implement true privacy in a JavaScript class?" expects mention of private fields (#field) as the modern answer, with the closure pattern (Topic 6.4) as the older alternative for when classes aren't used.

Topic 8.7 — Interview Lens: this, Classes, and OO Patterns — covers the deeper interview treatment.

Try It

Exercise: Build a Library class:

  1. A private field #books holding an array of book objects
  2. A static field LIBRARY_NAME (default "AlgoInsight Library")
  3. A static factory method createWithBooks(books) that creates a new library and adds the given books
  4. Instance methods addBook(book), removeBook(title), findBook(title)
  5. A getter bookCount returning the number of books
  6. A private method #normalizeTitle(title) that lowercases and trims a title — used internally by findBook and removeBook to make searches case-insensitive

Then verify:

  • You can create a library with Library.createWithBooks([...])
  • You cannot access #books directly from outside
  • You cannot call #normalizeTitle from outside
  • findBook("THE GREAT GATSBY") still works even if the book was added with mixed case

By the time this works, you've exercised every modern class feature — private fields, private methods, static fields, static factories, getters.

Back to JavaScript Basic to Advance