Back to Blog
JavaScriptClassesPrivate FieldsEncapsulation

JavaScript Private Class Fields Guide

Master JavaScript private class fields with # syntax for true encapsulation and data privacy.

B
Bootspring Team
Engineering
October 18, 2019
6 min read

Private class fields use the # prefix for true encapsulation. Here's how to use them effectively.

Basic Syntax#

1class Counter { 2 // Private field 3 #count = 0; 4 5 increment() { 6 this.#count++; 7 } 8 9 decrement() { 10 this.#count--; 11 } 12 13 getCount() { 14 return this.#count; 15 } 16} 17 18const counter = new Counter(); 19counter.increment(); 20counter.increment(); 21console.log(counter.getCount()); // 2 22 23// Cannot access private field from outside 24console.log(counter.#count); // SyntaxError 25console.log(counter['#count']); // undefined

Private Methods#

1class BankAccount { 2 #balance = 0; 3 #transactionHistory = []; 4 5 // Private method 6 #logTransaction(type, amount) { 7 this.#transactionHistory.push({ 8 type, 9 amount, 10 timestamp: new Date(), 11 balance: this.#balance, 12 }); 13 } 14 15 // Private validation 16 #validateAmount(amount) { 17 if (typeof amount !== 'number' || amount <= 0) { 18 throw new Error('Invalid amount'); 19 } 20 } 21 22 deposit(amount) { 23 this.#validateAmount(amount); 24 this.#balance += amount; 25 this.#logTransaction('deposit', amount); 26 } 27 28 withdraw(amount) { 29 this.#validateAmount(amount); 30 if (amount > this.#balance) { 31 throw new Error('Insufficient funds'); 32 } 33 this.#balance -= amount; 34 this.#logTransaction('withdrawal', amount); 35 } 36 37 getBalance() { 38 return this.#balance; 39 } 40 41 getHistory() { 42 // Return copy to prevent external modification 43 return [...this.#transactionHistory]; 44 } 45}

Private Static Fields#

1class ApiClient { 2 // Private static field 3 static #baseUrl = 'https://api.example.com'; 4 static #instances = 0; 5 6 // Private instance field 7 #token; 8 9 constructor(token) { 10 this.#token = token; 11 ApiClient.#instances++; 12 } 13 14 // Private static method 15 static #buildUrl(endpoint) { 16 return `${ApiClient.#baseUrl}${endpoint}`; 17 } 18 19 async fetch(endpoint) { 20 const url = ApiClient.#buildUrl(endpoint); 21 const response = await fetch(url, { 22 headers: { 23 Authorization: `Bearer ${this.#token}`, 24 }, 25 }); 26 return response.json(); 27 } 28 29 // Public static method accessing private static 30 static getInstanceCount() { 31 return ApiClient.#instances; 32 } 33} 34 35const client = new ApiClient('secret-token'); 36console.log(ApiClient.getInstanceCount()); // 1

Private Accessors#

1class Temperature { 2 #celsius = 0; 3 4 // Private getter 5 get #kelvin() { 6 return this.#celsius + 273.15; 7 } 8 9 // Private setter 10 set #kelvin(value) { 11 this.#celsius = value - 273.15; 12 } 13 14 // Public interface 15 get celsius() { 16 return this.#celsius; 17 } 18 19 set celsius(value) { 20 if (value < -273.15) { 21 throw new Error('Below absolute zero'); 22 } 23 this.#celsius = value; 24 } 25 26 get fahrenheit() { 27 return (this.#celsius * 9) / 5 + 32; 28 } 29 30 set fahrenheit(value) { 31 this.celsius = ((value - 32) * 5) / 9; 32 } 33 34 // Internal use of private accessor 35 convertFromKelvin(kelvin) { 36 this.#kelvin = kelvin; 37 return this.#celsius; 38 } 39}

Checking for Private Fields#

1class MyClass { 2 #privateField = 42; 3 4 // Check if object has private field 5 static hasPrivateField(obj) { 6 try { 7 // Accessing private field throws if not present 8 return #privateField in obj; 9 } catch { 10 return false; 11 } 12 } 13 14 // Alternative using 'in' operator 15 static isInstance(obj) { 16 return #privateField in obj; 17 } 18} 19 20const instance = new MyClass(); 21console.log(MyClass.isInstance(instance)); // true 22console.log(MyClass.isInstance({})); // false 23console.log(MyClass.isInstance(null)); // false

Private Fields in Inheritance#

1class Animal { 2 #name; 3 4 constructor(name) { 5 this.#name = name; 6 } 7 8 getName() { 9 return this.#name; 10 } 11} 12 13class Dog extends Animal { 14 #breed; 15 16 constructor(name, breed) { 17 super(name); 18 this.#breed = breed; 19 } 20 21 // Cannot access parent's #name directly 22 // This would be a different #name field 23 describe() { 24 return `${this.getName()} is a ${this.#breed}`; 25 } 26} 27 28const dog = new Dog('Max', 'Labrador'); 29console.log(dog.describe()); // 'Max is a Labrador' 30 31// Each class has its own private namespace 32class Cat extends Animal { 33 #name = 'Override'; // Different from Animal's #name 34 35 getCatName() { 36 return this.#name; // Returns 'Override' 37 } 38}

Factory Pattern with Privacy#

1class SecureUser { 2 #password; 3 #email; 4 #role; 5 6 // Private constructor 7 constructor() { 8 throw new Error('Use SecureUser.create()'); 9 } 10 11 // Factory method 12 static create(email, password, role = 'user') { 13 const instance = Object.create(SecureUser.prototype); 14 instance.#email = email; 15 instance.#password = SecureUser.#hashPassword(password); 16 instance.#role = role; 17 return instance; 18 } 19 20 // Private static method 21 static #hashPassword(password) { 22 // Simple hash for example 23 return btoa(password); 24 } 25 26 verifyPassword(password) { 27 return SecureUser.#hashPassword(password) === this.#password; 28 } 29 30 get email() { 31 return this.#email; 32 } 33 34 hasRole(role) { 35 return this.#role === role; 36 } 37} 38 39const user = SecureUser.create('user@example.com', 'secret123'); 40console.log(user.verifyPassword('secret123')); // true 41console.log(user.email); // 'user@example.com'

State Machine#

1class TrafficLight { 2 static #STATES = ['red', 'yellow', 'green']; 3 #currentIndex = 0; 4 #intervalId = null; 5 6 #transition() { 7 this.#currentIndex = (this.#currentIndex + 1) % TrafficLight.#STATES.length; 8 this.#notify(); 9 } 10 11 #notify() { 12 const event = new CustomEvent('statechange', { 13 detail: { state: this.state }, 14 }); 15 this.dispatchEvent(event); 16 } 17 18 get state() { 19 return TrafficLight.#STATES[this.#currentIndex]; 20 } 21 22 start(interval = 3000) { 23 if (this.#intervalId) return; 24 this.#intervalId = setInterval(() => this.#transition(), interval); 25 } 26 27 stop() { 28 if (this.#intervalId) { 29 clearInterval(this.#intervalId); 30 this.#intervalId = null; 31 } 32 } 33} 34 35// Add EventTarget functionality 36Object.assign(TrafficLight.prototype, EventTarget.prototype);

Immutable Configuration#

1class Config { 2 #settings; 3 #frozen = false; 4 5 constructor(initialSettings = {}) { 6 this.#settings = { ...initialSettings }; 7 } 8 9 get(key) { 10 return this.#settings[key]; 11 } 12 13 set(key, value) { 14 if (this.#frozen) { 15 throw new Error('Config is frozen'); 16 } 17 this.#settings[key] = value; 18 return this; 19 } 20 21 freeze() { 22 this.#frozen = true; 23 return this; 24 } 25 26 isFrozen() { 27 return this.#frozen; 28 } 29 30 // Return copy, not reference 31 toObject() { 32 return { ...this.#settings }; 33 } 34} 35 36const config = new Config({ debug: true }) 37 .set('apiUrl', 'https://api.example.com') 38 .set('timeout', 5000) 39 .freeze(); 40 41config.set('debug', false); // Error: Config is frozen

Observable with Private State#

1class Observable { 2 #value; 3 #subscribers = new Set(); 4 5 constructor(initialValue) { 6 this.#value = initialValue; 7 } 8 9 get value() { 10 return this.#value; 11 } 12 13 set value(newValue) { 14 const oldValue = this.#value; 15 this.#value = newValue; 16 this.#notifySubscribers(newValue, oldValue); 17 } 18 19 #notifySubscribers(newValue, oldValue) { 20 this.#subscribers.forEach((callback) => { 21 callback(newValue, oldValue); 22 }); 23 } 24 25 subscribe(callback) { 26 this.#subscribers.add(callback); 27 28 // Return unsubscribe function 29 return () => { 30 this.#subscribers.delete(callback); 31 }; 32 } 33} 34 35const count = new Observable(0); 36const unsubscribe = count.subscribe((newVal, oldVal) => { 37 console.log(`Changed from ${oldVal} to ${newVal}`); 38}); 39 40count.value = 1; // Changed from 0 to 1 41count.value = 2; // Changed from 1 to 2 42unsubscribe(); 43count.value = 3; // No log

Best Practices#

Usage: ✓ Use # for true privacy ✓ Keep sensitive data private ✓ Hide implementation details ✓ Use private methods for internals Patterns: ✓ Validation in private methods ✓ State management ✓ Factory methods ✓ Encapsulated logic Benefits: ✓ True encapsulation ✓ No naming conventions needed ✓ Cannot be accessed externally ✓ Safe from prototype pollution Avoid: ✗ Private fields in object literals ✗ Accessing via ['#name'] ✗ Exposing private data in methods ✗ Over-privatizing everything

Conclusion#

Private class fields with # provide true encapsulation in JavaScript. They cannot be accessed or modified from outside the class, making them ideal for sensitive data and internal implementation. Use private methods for validation and internal logic, and remember that each class has its own private namespace even in inheritance.

Share this article

Help spread the word about Bootspring