私有字段与方法
私有字段与方法
封装是面向对象编程的核心原则之一,它允许我们隐藏对象的内部实现细节。本文将介绍JavaScript中实现私有字段和方法的多种方式,特别是ES2022引入的私有字段语法。
私有字段基础
使用#前缀定义私有字段
从ES2022开始,JavaScript支持使用#
前缀定义真正的私有字段:
class Person {
// 私有字段
#name;
#age;
// 公共字段
occupation;
constructor(name, age, occupation) {
this.#name = name;
this.#age = age;
this.occupation = occupation;
}
introduce() {
return `我叫${this.#name},今年${this.#age}岁,是一名${this.occupation}。`;
}
// 访问私有字段的公共方法
getName() {
return this.#name;
}
getAge() {
return this.#age;
}
// 修改私有字段的公共方法
setName(name) {
if (name && name.length > 0) {
this.#name = name;
}
}
setAge(age) {
if (age >= 0 && age <= 150) {
this.#age = age;
}
}
}
const person = new Person('张三', 30, '工程师');
console.log(person.introduce()); // 输出:我叫张三,今年30岁,是一名工程师。
// 访问公共字段
console.log(person.occupation); // 输出:工程师
person.occupation = '设计师';
console.log(person.occupation); // 输出:设计师
// 通过公共方法访问私有字段
console.log(person.getName()); // 输出:张三
person.setName('李四');
console.log(person.getName()); // 输出:李四
// 错误:无法直接访问私有字段
// console.log(person.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
// person.#age = 25; // SyntaxError: Private field '#age' must be declared in an enclosing class
私有字段的特点
- 真正的私有性:私有字段只能在类的内部访问,外部无法直接读取或修改
- 语法检查:在类外部访问私有字段会导致语法错误,而不是运行时错误
- 不可枚举:私有字段不会出现在
Object.keys()
或for...in
循环中 - 命名冲突:不同类的私有字段可以使用相同的名称而不会冲突
class Example {
#field = 'private';
publicField = 'public';
showFields() {
console.log(this.#field); // 可以在类内部访问
console.log(this.publicField);
}
}
const example = new Example();
example.showFields(); // 输出:private public
// 枚举属性
console.log(Object.keys(example)); // 输出:['publicField']
// 检查属性是否存在
console.log('publicField' in example); // 输出:true
console.log('#field' in example); // 输出:false(私有字段不能通过in运算符检测)
私有方法
使用#前缀定义私有方法
与私有字段类似,私有方法也使用#
前缀定义:
class BankAccount {
#balance = 0;
constructor(initialBalance) {
if (initialBalance > 0) {
this.#deposit(initialBalance);
}
}
// 私有方法
#validateAmount(amount) {
if (typeof amount !== 'number' || amount <= 0) {
throw new Error('金额必须是正数');
}
return true;
}
#deposit(amount) {
if (this.#validateAmount(amount)) {
this.#balance += amount;
return true;
}
return false;
}
#withdraw(amount) {
if (this.#validateAmount(amount)) {
if (amount > this.#balance) {
throw new Error('余额不足');
}
this.#balance -= amount;
return true;
}
return false;
}
// 公共方法
getBalance() {
return this.#balance;
}
deposit(amount) {
return this.#deposit(amount);
}
withdraw(amount) {
return this.#withdraw(amount);
}
transfer(amount, targetAccount) {
if (this.#withdraw(amount)) {
targetAccount.deposit(amount);
return true;
}
return false;
}
}
const account1 = new BankAccount(1000);
const account2 = new BankAccount(500);
console.log(account1.getBalance()); // 输出:1000
console.log(account2.getBalance()); // 输出:500
account1.deposit(200);
console.log(account1.getBalance()); // 输出:1200
account1.transfer(300, account2);
console.log(account1.getBalance()); // 输出:900
console.log(account2.getBalance()); // 输出:800
// 错误:无法直接访问私有方法
// account1.#deposit(100); // SyntaxError
// account1.#withdraw(100); // SyntaxError
私有静态方法
类也可以有私有静态方法:
class MathUtils {
// 私有静态方法
static #calculateFactorial(n) {
if (n <= 1) return 1;
return n * this.#calculateFactorial(n - 1);
}
// 公共静态方法
static factorial(n) {
if (typeof n !== 'number' || n < 0 || !Number.isInteger(n)) {
throw new Error('参数必须是非负整数');
}
return this.#calculateFactorial(n);
}
static #isPrime(n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 === 0 || n % 3 === 0) return false;
for (let i = 5; i * i <= n; i += 6) {
if (n % i === 0 || n % (i + 2) === 0) return false;
}
return true;
}
static getPrimes(max) {
const primes = [];
for (let i = 2; i <= max; i++) {
if (this.#isPrime(i)) {
primes.push(i);
}
}
return primes;
}
}
console.log(MathUtils.factorial(5)); // 输出:120
console.log(MathUtils.getPrimes(20)); // 输出:[2, 3, 5, 7, 11, 13, 17, 19]
// 错误:无法直接访问私有静态方法
// MathUtils.#calculateFactorial(5); // SyntaxError
// MathUtils.#isPrime(7); // SyntaxError
私有字段的继承
私有字段不会被子类继承,但子类可以通过父类的公共方法间接访问:
class Parent {
#privateField = 'private';
getPrivateField() {
return this.#privateField;
}
setPrivateField(value) {
this.#privateField = value;
}
}
class Child extends Parent {
showPrivateField() {
// 错误:子类不能直接访问父类的私有字段
// console.log(this.#privateField); // SyntaxError
// 正确:通过父类的公共方法访问
console.log(this.getPrivateField());
}
updatePrivateField(value) {
this.setPrivateField(value);
}
}
const child = new Child();
child.showPrivateField(); // 输出:private
child.updatePrivateField('updated');
child.showPrivateField(); // 输出:updated
子类可以定义与父类同名的私有字段,它们是完全独立的:
class Parent {
#field = 'parent';
getParentField() {
return this.#field;
}
}
class Child extends Parent {
#field = 'child'; // 与父类的#field完全不同
getChildField() {
return this.#field;
}
showBothFields() {
console.log(`父类字段: ${this.getParentField()}`);
console.log(`子类字段: ${this.getChildField()}`);
}
}
const child = new Child();
child.showBothFields();
// 输出:
// 父类字段: parent
// 子类字段: child
私有字段的替代方案
在私有字段语法出现之前,JavaScript开发者使用了多种方式模拟私有成员:
1. 闭包和IIFE
使用闭包可以创建对外部不可见的变量:
const Person = (function() {
// 在闭包中定义"私有"变量
const privateData = new WeakMap();
return class Person {
constructor(name, age) {
privateData.set(this, { name, age });
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
setName(name) {
privateData.get(this).name = name;
}
setAge(age) {
privateData.get(this).age = age;
}
};
})();
const person = new Person('张三', 30);
console.log(person.getName()); // 输出:张三
console.log(person.getAge()); // 输出:30
// 无法直接访问私有数据
console.log(person.name); // 输出:undefined
2. Symbol作为属性键
使用Symbol作为属性键可以创建不容易被意外访问的属性:
const _name = Symbol('name');
const _age = Symbol('age');
class Person {
constructor(name, age) {
this[_name] = name;
this[_age] = age;
}
getName() {
return this[_name];
}
getAge() {
return this[_age];
}
}
const person = new Person('张三', 30);
console.log(person.getName()); // 输出:张三
// Symbol属性不会出现在常规枚举中
console.log(Object.keys(person)); // 输出:[]
// 但仍然可以通过Symbol引用访问
console.log(person[_name]); // 输出:张三
// 或者通过反射API
console.log(Reflect.ownKeys(person)); // 输出:[Symbol(name), Symbol(age)]
3. 下划线命名约定
使用下划线前缀表示私有成员是一种常见的约定(但不提供真正的私有性):
class Person {
constructor(name, age) {
this._name = name; // 约定为"私有"
this._age = age; // 约定为"私有"
}
getName() {
return this._name;
}
getAge() {
return this._age;
}
}
const person = new Person('张三', 30);
// 虽然有约定,但仍可直接访问
console.log(person._name); // 输出:张三
person._age = 25; // 可以直接修改
私有字段的最佳实践
1. 封装实现细节
使用私有字段隐藏实现细节,只暴露必要的公共API:
class LinkedList {
#head = null;
#tail = null;
#size = 0;
// 私有节点类
#Node = class {
constructor(data) {
this.data = data;
this.next = null;
}
};
get size() {
return this.#size;
}
isEmpty() {
return this.#size === 0;
}
add(data) {
const newNode = new this.#Node(data);
if (this.#head === null) {
this.#head = newNode;
this.#tail = newNode;
} else {
this.#tail.next = newNode;
this.#tail = newNode;
}
this.#size++;
return this;
}
toArray() {
const array = [];
let current = this.#head;
while (current !== null) {
array.push(current.data);
current = current.next;
}
return array;
}
}
const list = new LinkedList();
list.add(1).add(2).add(3);
console.log(list.size); // 输出:3
console.log(list.toArray()); // 输出:[1, 2, 3]
// 无法访问内部实现
// console.log(list.#head); // SyntaxError
// console.log(list.#Node); // SyntaxError
2. 数据验证和约束
使用私有字段和公共访问器方法可以实现数据验证:
class Temperature {
#celsius;
constructor(celsius) {
this.celsius = celsius; // 使用setter进行验证
}
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (typeof value !== 'number') {
throw new TypeError('温度必须是数字');
}
if (value < -273.15) {
throw new RangeError('温度不能低于绝对零度 (-273.15°C)');
}
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9/5 + 32;
}
set fahrenheit(value) {
if (typeof value !== 'number') {
throw new TypeError('温度必须是数字');
}
// 转换为摄氏度并验证
this.celsius = (value - 32) * 5/9;
}
get kelvin() {
return this.#celsius + 273.15;
}
set kelvin(value) {
if (typeof value !== 'number') {
throw new TypeError('温度必须是数字');
}
if (value < 0) {
throw new RangeError('开尔文温度不能为负');
}
this.#celsius = value - 273.15;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 输出:25
console.log(temp.fahrenheit); // 输出:77
console.log(temp.kelvin); // 输出:298.15
temp.celsius = 30;
console.log(temp.fahrenheit); // 输出:86
temp.fahrenheit = 50;
console.log(temp.celsius); // 输出:10
// 错误:无效的温度值
// temp.celsius = -300; // RangeError: 温度不能低于绝对零度 (-273.15°C)
3. 状态管理与不变性
私有字段可以帮助管理对象的内部状态,确保状态变更的一致性:
class Counter {
#count = 0;
#min;
#max;
#callbacks = [];
constructor(options = {}) {
this.#min = options.min !== undefined ? options.min : Number.MIN_SAFE_INTEGER;
this.#max = options.max !== undefined ? options.max : Number.MAX_SAFE_INTEGER;
if (options.initial !== undefined) {
this.#setCount(options.initial);
}
}
get count() {
return this.#count;
}
#setCount(newCount) {
if (newCount < this.#min || newCount > this.#max) {
throw new RangeError(`计数必须在 ${this.#min} 和 ${this.#max} 之间`);
}
const oldCount = this.#count;
this.#count = newCount;
// 触发回调
this.#callbacks.forEach(callback => callback(newCount, oldCount));
return this;
}
increment(step = 1) {
return this.#setCount(this.#count + step);
}
decrement(step = 1) {
return this.#setCount(this.#count - step);
}
reset() {
return this.#setCount(0);
}
// 注册状态变更回调
onChange(callback) {
if (typeof callback === 'function') {
this.#callbacks.push(callback);
}
return this;
}
}
const counter = new Counter({ min: 0, max: 10, initial: 5 });
counter.onChange((newCount, oldCount) => {
console.log(`计数从 ${oldCount} 变为 ${newCount}`);
});
counter.increment(); // 输出:计数从 5 变为 6
counter.increment(2); // 输出:计数从 6 变为 8
counter.decrement(3); // 输出:计数从 8 变为 5
counter.reset(); // 输出:计数从 5 变为 0
// 错误:超出范围
// counter.decrement(); // RangeError: 计数必须在 0 和 10 之间
实际应用示例
1. 自定义事件发射器
使用私有字段实现事件处理系统:
class EventEmitter {
#events = new Map();
#maxListeners = 10;
on(event, listener) {
this.#addListener(event, listener, false);
return this;
}
once(event, listener) {
this.#addListener(event, listener, true);
return this;
}
#addListener(event, listener, once) {
if (typeof listener !== 'function') {
throw new TypeError('监听器必须是函数');
}
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
const listeners = this.#events.get(event);
// 警告:监听器过多可能是内存泄漏
if (listeners.length >= this.#maxListeners) {
console.warn(`事件 "${event}" 的监听器数量超过了最大值 ${this.#maxListeners}`);
}
listeners.push({
fn: listener,
once
});
}
off(event, listener) {
if (!this.#events.has(event)) return this;
if (!listener) {
this.#events.delete(event);
return this;
}
const listeners = this.#events.get(event);
const newListeners = listeners.filter(item => item.fn !== listener);
if (newListeners.length === 0) {
this.#events.delete(event);
} else {
this.#events.set(event, newListeners);
}
return this;
}
emit(event, ...args) {
if (!this.#events.has(event)) return false;
const listeners = this.#events.get(event);
const onceListeners = [];
listeners.forEach((item, index) => {
item.fn(...args);
if (item.once) {
onceListeners.push(index);
}
});
// 移除一次性监听器
if (onceListeners.length > 0) {
const remainingListeners = listeners.filter((_, index) => !onceListeners.includes(index));
if (remainingListeners.length === 0) {
this.#events.delete(event);
} else {
this.#events.set(event, remainingListeners);
}
}
return true;
}
listenerCount(event) {
if (!this.#events.has(event)) return 0;
return this.#events.get(event).length;
}
setMaxListeners(n) {
if (typeof n !== 'number' || n < 0) {
throw new TypeError('最大监听器数量必须是非负数');
}
this.#maxListeners = n;
return this;
}
}
// 使用示例
const emitter = new EventEmitter();
// 添加监听器
emitter.on('message', data => {
console.log('收到消息:', data);
});
emitter.once('connect', () => {
console.log('已连接(只触发一次)');
});
// 触发事件
emitter.emit('connect'); // 输出:已连接(只触发一次)
emitter.emit('connect'); // 不会再次触发
emitter.emit('message', '你好'); // 输出:收到消息: 你好
emitter.emit('message', '世界'); // 输出:收到消息: 世界
console.log(emitter.listenerCount('message')); // 输出:1
console.log(emitter.listenerCount('connect')); // 输出:0(一次性监听器已移除)
2. 状态机实现
使用私有字段实现简单的状态机:
class StateMachine {
#state;
#transitions = new Map();
#stateChangeListeners = [];
constructor(initialState) {
this.#state = initialState;
}
get state() {
return this.#state;
}
addTransition(fromState, event, toState, action = null) {
if (!this.#transitions.has(fromState)) {
this.#transitions.set(fromState, new Map());
}
const stateTransitions = this.#transitions.get(fromState);
stateTransitions.set(event, { toState, action });
return this;
}
trigger(event, ...args) {
const currentState = this.#state;
if (!this.#transitions.has(currentState)) {
console.warn(`当前状态 "${currentState}" 没有定义任何转换`);
return false;
}
const stateTransitions = this.#transitions.get(currentState);
if (!stateTransitions.has(event)) {
console.warn(`当前状态 "${currentState}" 不支持事件 "${event}"`);
return false;
}
const { toState, action } = stateTransitions.get(event);
const oldState = this.#state;
this.#state = toState;
// 执行转换动作
if (typeof action === 'function') {
action(...args);
}
// 通知状态变更
this.#notifyStateChange(oldState, toState, event);
return true;
}
#notifyStateChange(oldState, newState, event) {
this.#stateChangeListeners.forEach(listener => {
listener(oldState, newState, event);
});
}
onStateChange(listener) {
if (typeof listener === 'function') {
this.#stateChangeListeners.push(listener);
}
return this;
}
canTrigger(event) {
const currentState = this.#state;
if (!this.#transitions.has(currentState)) {
return false;
}
return this.#transitions.get(currentState).has(event);
}
}
// 使用示例:简单的订单状态机
const orderStateMachine = new StateMachine('created');
// 定义状态转换
orderStateMachine
.addTransition('created', 'confirm', 'confirmed', () => {
console.log('订单已确认');
})
.addTransition('confirmed', 'ship', 'shipped', () => {
console.log('订单已发货');
})
.addTransition('shipped', 'deliver', 'delivered', () => {
console.log('订单已送达');
})
.addTransition('delivered', 'complete', 'completed', () => {
console.log('订单已完成');
})
.addTransition('created', 'cancel', 'cancelled', () => {
console.log('订单已取消');
})
.addTransition('confirmed', 'cancel', 'cancelled', () => {
console.log('订单已取消(已确认)');
});
// 监听状态变更
orderStateMachine.onStateChange((oldState, newState, event) => {
console.log(`状态从 ${oldState} 变为 ${newState},触发事件: ${event}`);
});
// 触发状态转换
console.log('当前状态:', orderStateMachine.state); // 输出:当前状态: created
orderStateMachine.trigger('confirm'); // 触发确认事件
console.log('当前状态:', orderStateMachine.state); // 输出:当前状态: confirmed
orderStateMachine.trigger('ship'); // 触发发货事件
console.log('当前状态:', orderStateMachine.state); // 输出:当前状态: shipped
// 检查是否可以触发事件
console.log('可以取消?', orderStateMachine.canTrigger('cancel')); // 输出:可以取消? false
console.log('可以送达?', orderStateMachine.canTrigger('deliver')); // 输出:可以送达? true
私有字段与TypeScript
TypeScript提供了更丰富的访问修饰符,包括private
、protected
和public
:
class Person {
// TypeScript的访问修饰符
private name: string;
protected age: number;
public occupation: string;
constructor(name: string, age: number, occupation: string) {
this.name = name;
this.age = age;
this.occupation = occupation;
}
public introduce(): string {
return `我叫${this.name},今年${this.age}岁,是一名${this.occupation}。`;
}
}
class Employee extends Person {
private employeeId: string;
constructor(name: string, age: number, occupation: string, employeeId: string) {
super(name, age, occupation);
this.employeeId = employeeId;
}
public getDetails(): string {
// 可以访问protected成员
return `员工ID: ${this.employeeId}, 年龄: ${this.age}`;
// 错误:无法访问private成员
// return `员工ID: ${this.employeeId}, 姓名: ${this.name}`;
}
}
需要注意的是,TypeScript的访问修饰符只在编译时检查,运行时并不提供真正的私有性。如果需要运行时的私有性,仍然需要使用JavaScript的#
私有字段。
总结
私有字段和方法是JavaScript面向对象编程中实现封装的重要工具,通过本文,我们学习了:
- 私有字段基础:使用
#
前缀定义真正的私有字段,及其特点 - 私有方法:定义和使用私有实例方法和静态方法
- 私有字段的继承:私有字段不会被继承,但可以通过公共方法间接访问
- 私有字段的替代方案:在
#
语法出现前的模拟私有成员的方法 - 最佳实践:如何使用私有字段封装实现细节、进行数据验证和状态管理
- 实际应用示例:在事件发射器和状态机中应用私有字段
私有字段和方法的引入使JavaScript的面向对象编程更加完善,让开发者能够更好地实现封装,提高代码的可维护性和安全性。在设计类时,合理使用私有成员可以隐藏实现细节,减少外部依赖,并提供更清晰的公共API接口。
随着JavaScript语言的不断发展,私有字段语法(#前缀)已成为实现真正私有性的标准方式,它比早期的模拟方案(如闭包、Symbol或命名约定)提供了更好的性能和更严格的封装保证。掌握这一特性对于编写高质量的面向对象JavaScript代码至关重要。