private class fields
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:
- Accessible only within the class where they are declared
- Subclasses cannot access private fields of the parent class
- Instance methods can access private fields
- 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:
- Must be declared before use
- Cannot be accessed via dynamic property names
- Cannot be retrieved by reflection methods like
Object.keys()
orJSON.stringify()
- 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
上一篇:私有类方法和访问器
下一篇:静态类字段和静态私有方法