阿里云主机折上折
  • 微信号
Current Site:Index > private class fields

private class fields

Author:Chuan Chen 阅读数:43355人阅读 分类: JavaScript

Basic Concepts of Private Class Fields

ECMAScript 12 introduced private class fields, a significant extension to JavaScript's class syntax. Private fields are declared by prefixing the field name with #, making them accessible only within the class and not directly readable or modifiable from the outside. This encapsulation mechanism addresses issues with the traditional _ prefix convention, providing true privacy.

class Counter {
  #count = 0;  // Private field

  increment() {
    this.#count++;
  }

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

const counter = new Counter();
counter.increment();
console.log(counter.value); // 1
console.log(counter.#count); // Error: Private field cannot be accessed outside the class

Declaration and Initialization of Private Fields

Private fields must be declared at the top level of the class. They can be initialized at declaration or within the constructor. Unlike public fields, private fields are not automatically initialized to undefined—accessing them before initialization throws a reference error.

class User {
  #name;  // Declare private field
  #age = 0;  // Declare and initialize

  constructor(name, age) {
    this.#name = name;  // Initialize in constructor
    this.#age = age;
  }

  get profile() {
    return `${this.#name}, ${this.#age} years old`;
  }
}

const user = new User("Zhang San", 25);
console.log(user.profile); // "Zhang San, 25 years old"

Access Rules for Private Fields

Private fields have strict access control rules:

  1. Accessible only within the class where they are declared
  2. Subclasses cannot access private fields of the parent class
  3. Instance methods can access private fields
  4. Static methods cannot access instance private fields
class Parent {
  #secret = "Parent's secret";

  reveal() {
    return this.#secret;
  }
}

class Child extends Parent {
  tryReveal() {
    // return this.#secret; // Error: Subclass cannot access parent's private field
    return this.reveal();  // Indirect access via parent method
  }
}

const child = new Child();
console.log(child.tryReveal()); // "Parent's secret"

Private Fields and Methods

Private fields can be used with class methods to implement more complex encapsulation logic. Methods can freely access private fields, while external callers can only interact with these fields through public interfaces.

class BankAccount {
  #balance = 0;
  #transactionHistory = [];

  deposit(amount) {
    if (amount <= 0) throw new Error("Deposit amount must be greater than 0");
    this.#balance += amount;
    this.#recordTransaction(`Deposit: +${amount}`);
  }

  withdraw(amount) {
    if (amount > this.#balance) throw new Error("Insufficient balance");
    this.#balance -= amount;
    this.#recordTransaction(`Withdrawal: -${amount}`);
  }

  #recordTransaction(description) {
    this.#transactionHistory.push({
      description,
      date: new Date(),
      balance: this.#balance
    });
  }

  get statement() {
    return [...this.#transactionHistory];
  }
}

const account = new BankAccount();
account.deposit(1000);
account.withdraw(500);
console.log(account.statement);

Private Static Fields

In addition to instance private fields, ECMAScript 12 also supports private static fields, which belong to the class itself rather than instances. They are declared by prefixing static with #.

class Logger {
  static #logLevel = "info";
  static #supportedLevels = ["error", "warn", "info", "debug"];

  static setLevel(level) {
    if (!this.#supportedLevels.includes(level)) {
      throw new Error(`Unsupported log level: ${level}`);
    }
    this.#logLevel = level;
  }

  static log(message, level = "info") {
    if (this.#supportedLevels.indexOf(level) > 
        this.#supportedLevels.indexOf(this.#logLevel)) {
      return;
    }
    console.log(`[${level.toUpperCase()}] ${message}`);
  }
}

Logger.setLevel("debug");
Logger.log("Debug message", "debug"); // Outputs debug message
Logger.log("Regular message"); // No output, level below debug

Private Fields vs. TypeScript's private Modifier

Although TypeScript has a private modifier, it is only a compile-time constraint—these fields remain public in the compiled JavaScript code. ECMAScript's private fields are truly private at the language level, maintaining privacy even at runtime.

// TypeScript code
class TSExample {
  private secret = "typescript private";
}

const ts = new TSExample();
console.log(ts["secret"]); // Compile-time error, but accessible at runtime

// ECMAScript private fields
class JSExample {
  #secret = "ecmascript private";
}

const js = new JSExample();
console.log(js["#secret"]); // undefined, truly private

Limitations and Considerations for Private Fields

When using private fields, note the following limitations:

  1. Must be declared before use
  2. Cannot be accessed via dynamic property names
  3. Cannot be retrieved by reflection methods like Object.keys() or JSON.stringify()
  4. Private fields are not shared between instances
class Example {
  #privateField;

  constructor(value) {
    this.#privateField = value;
  }

  tryDynamicAccess(key) {
    // return this[key]; // Cannot access private fields via dynamic keys
    return this.#privateField;
  }
}

const ex1 = new Example("Instance 1");
const ex2 = new Example("Instance 2");
console.log(ex1.tryDynamicAccess("#privateField")); // "Instance 1"
console.log(ex2.tryDynamicAccess("#privateField")); // "Instance 2"

Private Fields and Proxy

Private fields interact specially with Proxy. Since private field access does not follow the regular property access path, Proxy cannot intercept access to private fields.

class Target {
  #secret = "confidential";

  getSecret() {
    return this.#secret;
  }
}

const handler = {
  get(target, prop) {
    console.log(`Intercepted property access: ${prop}`);
    return target[prop];
  }
};

const proxy = new Proxy(new Target(), handler);
proxy.getSecret(); // Only outputs "Intercepted property access: getSecret", does not intercept private field access

Private Fields and Class Inheritance

Private fields have unique behavior in inheritance hierarchies: subclasses can declare private fields with the same name as those in the parent class, and they will be treated as entirely separate fields without conflict.

class Parent {
  #name = "Parent";

  getName() {
    return this.#name;
  }
}

class Child extends Parent {
  #name = "Child";  // Same name as parent's private field but no conflict

  getChildName() {
    return this.#name;
  }
}

const child = new Child();
console.log(child.getName()); // "Parent"
console.log(child.getChildName()); // "Child"

Detection and Reflection of Private Fields

Due to their true privacy, conventional reflection methods cannot detect private fields. However, the in operator can be used within the class to check for the existence of private fields.

class Test {
  #field;

  static hasPrivateField(instance) {
    return #field in instance;  // Private field detection
  }
}

const test = new Test();
console.log(Test.hasPrivateField(test)); // true
console.log("#field" in test); // false, cannot detect externally

Private Fields and Class Expressions

Private fields can be used not only in class declarations but also in class expressions, providing better encapsulation for creating one-off classes or dynamically generated classes.

const createCounter = () => {
  return class {
    #count = 0;

    increment() {
      this.#count++;
    }

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

const Counter = createCounter();
const counter = new Counter();
counter.increment();
console.log(counter.value); // 1

Private Fields and the Decorator Proposal

Although the decorator proposal has not yet been formally adopted into ECMAScript, the potential interaction between private fields and decorators is noteworthy. Decorators could be used to enhance private field functionality.

// Hypothetical usage if the decorator proposal is adopted
class DecoratedExample {
  @readonly
  #immutableValue = 42;

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

function readonly(target, context) {
  if (context.kind === "field" && context.private) {
    return function() {
      throw new Error("Cannot modify private readonly field");
    };
  }
}

Performance Considerations for Private Fields

Engines typically optimize private field implementations, making their access speed comparable to public fields. Since private fields' existence can be determined at compile time, they may even be faster than dynamic property access in some cases.

class PerformanceTest {
  publicField = 0;
  #privateField = 0;

  testPublic() {
    for (let i = 0; i < 1e6; i++) {
      this.publicField++;
    }
  }

  testPrivate() {
    for (let i = 0; i < 1e6; i++) {
      this.#privateField++;
    }
  }
}

Private Fields and Code Maintainability

Private fields improve code maintainability by enforcing encapsulation, clearly separating a class's internal implementation details from its public interface. This clear boundary allows refactoring internal implementations without accidentally breaking external code.

class ShoppingCart {
  #items = [];
  #discount = 0;

  addItem(item) {
    this.#items.push(item);
  }

  applyDiscount(percentage) {
    this.#discount = Math.min(percentage, 30); // Cap maximum discount at 30%
  }

  get total() {
    const subtotal = this.#items.reduce((sum, item) => sum + item.price, 0);
    return subtotal * (1 - this.#discount / 100);
  }
}

// External code can only interact with the cart via public methods
// Internal implementation can be freely modified without affecting consumers

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.