Docs

11.4-Private-Fields

8.4 Private Fields & Methods

Overview

ES2022 introduced true private class members using the # prefix. Private fields and methods are only accessible within the class body.

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│             MyClass                      │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│  Public Members:                        │
│  ā”œā”€ this.publicProp     (accessible)    │
│  └─ this.publicMethod() (accessible)    │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│  Private Members (#):                   │
│  ā”œā”€ this.#privateProp   (hidden)        │
│  └─ this.#privateMethod (hidden)        │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
        ↓                    ↓
    Accessible           SyntaxError
    from outside         if accessed

Private Instance Fields

class BankAccount {
  #balance = 0; // Private field
  #pin; // Private field (undefined initially)

  constructor(initialBalance, pin) {
    this.#balance = initialBalance;
    this.#pin = pin;
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  withdraw(amount, pin) {
    if (!this.#verifyPin(pin)) {
      throw new Error('Invalid PIN');
    }
    if (amount > this.#balance) {
      throw new Error('Insufficient funds');
    }
    this.#balance -= amount;
    return amount;
  }

  // Private method
  #verifyPin(pin) {
    return this.#pin === pin;
  }

  get balance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000, '1234');
console.log(account.balance); // 1000 (via getter)
// console.log(account.#balance); // SyntaxError: Private field

Private vs Public Comparison

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Aspect             │ Public              │ Private (#)         │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Syntax             │ this.prop           │ this.#prop          │
│ Declaration        │ Optional            │ Required in class   │
│ External Access    │ Yes                 │ No (SyntaxError)    │
│ Reflection         │ Yes (Object.keys)   │ No                  │
│ Subclass Access    │ Yes                 │ No                  │
│ Performance        │ Standard            │ Slightly faster     │
│ Debugging          │ Visible             │ Hidden in console   │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Private Methods

class User {
  #passwordHash;

  constructor(username, password) {
    this.username = username;
    this.#passwordHash = this.#hashPassword(password);
  }

  // Private method
  #hashPassword(password) {
    // Simplified hash (use proper crypto in production)
    return Buffer.from(password).toString('base64');
  }

  // Private validation
  #validatePassword(password) {
    return this.#hashPassword(password) === this.#passwordHash;
  }

  // Public interface
  authenticate(password) {
    return this.#validatePassword(password);
  }

  changePassword(oldPassword, newPassword) {
    if (!this.#validatePassword(oldPassword)) {
      throw new Error('Invalid current password');
    }
    this.#passwordHash = this.#hashPassword(newPassword);
  }
}

Private Static Members

class Config {
  // Private static field
  static #settings = new Map();
  static #initialized = false;

  // Private static method
  static #load() {
    if (this.#initialized) return;

    this.#settings.set('theme', 'light');
    this.#settings.set('language', 'en');
    this.#initialized = true;
  }

  // Public static interface
  static get(key) {
    this.#load();
    return this.#settings.get(key);
  }

  static set(key, value) {
    this.#load();
    this.#settings.set(key, value);
  }
}

console.log(Config.get('theme')); // "light"
// Config.#settings;  // SyntaxError

Private Getters and Setters

class Temperature {
  #celsius;

  constructor(celsius) {
    this.#celsius = celsius;
  }

  // Private getter
  get #kelvin() {
    return this.#celsius + 273.15;
  }

  // Private setter
  set #kelvin(value) {
    this.#celsius = value - 273.15;
  }

  // Public interface
  get celsius() {
    return this.#celsius;
  }

  set celsius(value) {
    this.#celsius = value;
  }

  get fahrenheit() {
    return (this.#celsius * 9) / 5 + 32;
  }

  // Internal use of private getter
  toKelvin() {
    return this.#kelvin;
  }
}

Checking for Private Fields

Use in operator to check if private field exists:

class User {
  #id;

  constructor(id) {
    this.#id = id;
  }

  static isUser(obj) {
    // Check if #id exists in obj
    return #id in obj;
  }
}

const user = new User(1);
console.log(User.isUser(user)); // true
console.log(User.isUser({})); // false

Private Fields in Inheritance

Private fields are NOT accessible in subclasses:

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Parent                                  │
│ ā”œā”€ this.publicProp     āœ“               │
│ ā”œā”€ this._protectedProp āœ“ (convention)  │
│ └─ this.#privateProp   āœ—               │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
          ↓ extends
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Child                                   │
│ ā”œā”€ Access this.publicProp     āœ“        │
│ ā”œā”€ Access this._protectedProp āœ“        │
│ └─ Access this.#privateProp   āœ—        │
│     └─ Must use parent's methods       │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
class Parent {
  #secret = 'hidden';
  _protected = 'accessible'; // Convention

  getSecret() {
    return this.#secret; // Accessor method
  }
}

class Child extends Parent {
  showData() {
    // console.log(this.#secret);  // SyntaxError
    console.log(this._protected); // Works (convention)
    console.log(this.getSecret()); // Works (via method)
  }
}

WeakMap Alternative (Pre-ES2022)

Before private fields, WeakMap was used:

// Old pattern (still works)
const _balance = new WeakMap();
const _pin = new WeakMap();

class BankAccountOld {
  constructor(balance, pin) {
    _balance.set(this, balance);
    _pin.set(this, pin);
  }

  get balance() {
    return _balance.get(this);
  }

  withdraw(amount, pin) {
    if (_pin.get(this) !== pin) {
      throw new Error('Invalid PIN');
    }
    const current = _balance.get(this);
    _balance.set(this, current - amount);
  }
}

// Modern pattern (ES2022+)
class BankAccountNew {
  #balance;
  #pin;

  constructor(balance, pin) {
    this.#balance = balance;
    this.#pin = pin;
  }

  get balance() {
    return this.#balance;
  }
}

Private Fields vs Symbol Properties

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ Aspect           │ Private Fields (#) │ Symbol Properties  │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ Truly Private    │ Yes                │ No (discoverable)  │
│ Inheritance      │ No access          │ Inherited          │
│ Reflection       │ Not visible        │ Object.getOwn...   │
│ Proxy Support    │ Tricky             │ Works normally     │
│ String Key       │ No                 │ Via Symbol.for     │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Common Patterns

Encapsulating State

class Counter {
  #count = 0;
  #max;
  #min;

  constructor({ min = 0, max = Infinity } = {}) {
    this.#min = min;
    this.#max = max;
    this.#count = min;
  }

  increment() {
    if (this.#count < this.#max) {
      this.#count++;
    }
    return this;
  }

  decrement() {
    if (this.#count > this.#min) {
      this.#count--;
    }
    return this;
  }

  get value() {
    return this.#count;
  }
}

Immutable-like Objects

class Point {
  #x;
  #y;

  constructor(x, y) {
    this.#x = x;
    this.#y = y;
    Object.freeze(this); // Prevent adding properties
  }

  get x() {
    return this.#x;
  }
  get y() {
    return this.#y;
  }

  // Returns new instance instead of mutating
  add(other) {
    return new Point(this.#x + other.x, this.#y + other.y);
  }

  scale(factor) {
    return new Point(this.#x * factor, this.#y * factor);
  }
}

Best Practices

Do āœ“Don't āœ—
Use # for true encapsulationExpose internal state unnecessarily
Provide public accessors when neededRely solely on underscore convention
Use private methods for internal logicMake everything private
Check #field in obj for type checkingTry to access private fields externally
Document public API clearlyForget that subclasses can't access

Key Takeaways

  1. •# prefix creates truly private members
  2. •Private fields must be declared in the class body
  3. •No external access - SyntaxError if tried
  4. •No inheritance access - use public methods
  5. •#field in obj checks for private field existence
  6. •Private statics work the same way
  7. •WeakMap is the pre-ES2022 alternative
.4 Private Fields - JavaScript Tutorial | DeepML