READMEJavaScript

README

Module 11 Classes / .4 Private Fields

Concept Lesson
Advanced
4 min

Learning Objective

Understand Module 11 Classes well enough to explain it, recognize it in JavaScript, and apply it in a small task.

Why It Matters

This concept is part of the foundation that later lessons and projects assume you already understand.

ClassesPrivate Instance FieldsPrivate Vs Public ComparisonPrivate MethodsPrivate Static Members
Private notes
0/8000

Notes stay private to your browser until account sync is configured.

README
1 min read15 headings

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

Skill Check

Test this lesson

Answer 4 quick questions to lock in the lesson and feed your adaptive practice queue.

--
Score
0/4
Answered
Not attempted
Status
1

Which module does this lesson belong to?

2

Which section is covered in this lesson content?

3

Which term is most central to this lesson?

4

What is the best way to use this lesson for real learning?

Your answers save locally first, then sync when account storage is available.
Practice queue