Chapter 7.5☕ 20 min

ES6 Classes: Private Fields, Static & super Internals

Class sugar hai, # private asli hai. Aur execution order mat bhoolna — child field parent constructor ko mita deta hai!

01Class Syntax: Sugar on Prototypes

Classes are syntactic sugar over constructor functions and prototypes. Under the hood, class Foo {} is roughly equivalent to function Foo() {} with methods on Foo.prototype.

Key differences from constructor functions: Classes are NOT hoisted like function declarations — they have a temporal dead zone. Classes always run in strict mode automatically. Classes cannot be called without new — throws TypeError. And typeof class {} === 'function' — a class IS a function at runtime.
// Class syntax
class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return 'Hi, ' + this.name;
  }
}

// Equivalent ES5 Constructor
function UserEs5(name) {
  this.name = name;
}
UserEs5.prototype.greet = function() {
  return 'Hi, ' + this.name;
};

const sai = new User('Sai');
console.log(typeof User); // 'function'
console.log(sai.greet()); // 'Hi, Sai'
02# Private Fields: True Engine-Level Privacy

ES2022 private fields start with #. They are truly private — not accessible outside the class body, not even via bracket notation or DevTools tricks.

Key rules: # fields are added to the instance, not the prototype. Accessing a private field on an object that doesn't have it throws TypeError (brand check). Private methods also exist: #privateMethod() {}. Static private: static #helper() {} — private to the class, not instances. Unlike closure-based privacy, # fields survive destructuring and prototype changes.

class BankAccount {
  #balance = 0; // Private field

  constructor(initial) {
    this.#balance = initial;
  }

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

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

  #validate(amount) { // Private method
    return amount > 0;
  }
}

const acc = new BankAccount(100);
acc.deposit(50);
console.log(acc.balance); // 150
// console.log(acc.#balance); // SyntaxError!
// acc.#validate(10);        // SyntaxError!

// Brand check: Objects without #balance throw TypeError
const fake = Object.create(acc);
// fake.#balance; // TypeError!
V8 brand check: V8 implements # private fields using a "brand" stored in the object's hidden class. When accessing #field, V8 checks if the object's hidden class has the brand. If not, it throws TypeError. This is a fast O(1) check, much faster than closure-based privacy.
03Static Fields & Static Blocks

Static fields and methods belong to the class itself, not instances. Accessed via ClassName.method().

Static fields are defined on the constructor function object. Static initialization blocks: static { ... } — runs once when the class is defined. Can access private static fields. Static #private: Only accessible from static methods or static blocks. this inside static methods refers to the class itself (useful for inheritance).

class AppConfig {
  static version = '1.0.0'; // Static field
  static #apiUrl = 'https://api.dev'; // Private static

  // Static initialization block
  static {
    console.log('AppConfig initialized');
    if (!this.#apiUrl) throw new Error('API URL missing');
  }

  static getApiUrl() {
    return this.#apiUrl; // Access private static
  }
}

console.log(AppConfig.version); // '1.0.0'
console.log(AppConfig.getApiUrl()); // 'https://api.dev'
// const cfg = new AppConfig();
// cfg.version; // undefined (static belongs to class)
Why static blocks? They let you run complex initialization logic at class definition time — something you previously had to do outside the class. Multiple static blocks are allowed and run in order. They're the only place to access private static fields during initialization.
04super & [[HomeObject]]: How Inheritance Resolves

super in a method resolves via the method's [[HomeObject]], which is set when the method is defined in a class or object literal.

super.method() is NOT Parent.prototype.method.call(this). It dynamically resolves based on [[HomeObject]]'s prototype. super() in constructor: Must be called before accessing this. Calls the parent constructor. If you extract a method from a class and call it standalone, super still works because [[HomeObject]] is bound at definition time.

class Animal {
  constructor(name) { this.name = name; }
  speak() { return this.name + ' makes a noise'; }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // Must call before 'this'
    this.type = 'dog';
  }
  speak() {
    return super.speak() + ', specifically a bark';
  }
}

const d = new Dog('Rex');
console.log(d.speak());
// 'Rex makes a noise, specifically a bark'
[[HomeObject]] deep dive: When V8 compiles a class method, it attaches a hidden [[HomeObject]] internal slot pointing to the class's prototype object. When super.speak() is called, V8 looks up [[HomeObject]]'s prototype (which is Animal.prototype) and calls speak there. This is why super still works even if you extract the method.
05Class Field Execution Order: The Trap

When creating a subclass instance, the execution order is critical and often misunderstood:

1. Parent class fields     → name = 'Parent Name'
2. Parent constructor body → this.name = 'Parent Constructor Name'
3. Child class fields      → name = 'Child Name' (OVERWRITES!)
4. Child constructor body   → runs after

This means child fields OVERWRITE values set by the parent constructor! This is the #1 source of bugs with class fields and inheritance.

class Parent {
  name = 'Parent Name'; // Step 1
  constructor() {
    console.log('Parent constructor'); // Step 2
    this.name = 'Parent Constructor Name';
  }
}

class Child extends Parent {
  name = 'Child Name'; // Step 3: OVERWRITES!
  constructor() {
    super();
    console.log('Child constructor'); // Step 4
  }
}

const c = new Child();
// Logs: "Parent constructor", "Child constructor"
console.log(c.name); // 'Child Name' — Parent's work overwritten!
📋 Golden Rule:
• Avoid initializing the same field in both parent class fields AND parent constructor
• Avoid defining the same field name in both parent and child class fields
• Choose ONE place for initialization — either class field OR constructor, not both

Lo kar liya — Key Points:

  • ✅ Classes are syntactic sugar over prototypes; typeof class is 'function'; they are always strict and require new
  • # private fields are true engine-level privacy, enforced by V8 brand checks, not closures
  • ✅ Static fields/methods belong to the class, not instances; static blocks run once at definition time
  • super in methods resolves via [[HomeObject]] (bound at definition); super() must be called before this in subclass constructors
  • ✅ Class field execution order: Parent fields → Parent constructor → Child fields → Child constructor
  • ✅ Child class fields overwrite values set by the parent constructor — a common inheritance trap
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase