先记住这个答案
在类体顶层写 #count = 0 就声明了私有字段,类里的方法用 this.#count 读写。它私有得很彻底:类外面写 obj.#count 在解析阶段就是 SyntaxError,代码根本跑不起来;Object.keys、for...in、JSON.stringify、Reflect.ownKeys 都拿不到它,因为它根本不是对象上的属性。以前用下划线 _count 或 TypeScript 的 private,都只是约定或编译期检查,运行时照样能读到。
- 私有字段必须先在类体里声明,名字带
# - 类外访问
#x会报SyntaxError - 反射和 JSON 都看不到
#私有字段 - 用
#x in obj判断对象是不是本类实例
最小例子:私有字段读写和品牌检查
下面这个计数器把 #count 藏起来,外部只能通过 increment() 改它。
class Counter {
#count = 0 // 私有字段,必须先在类体里声明
increment() {
this.#count++
return this.#count
}
static isCounter(obj) {
return #count in obj // 有这个私有字段才返回 true,不会抛错
}
static peek(obj) {
return obj.#count // obj 身上没有这个字段就抛 TypeError
}
}
const c = new Counter()
console.log(c.increment())
console.log(Object.keys(c).length, JSON.stringify(c))
console.log(Counter.isCounter(c), Counter.isCounter({}))
try {
Counter.peek({ '#count': 99 }) // 字符串键 '#count' 和私有字段毫无关系
} catch (e) {
console.log(e.name)
}查看输出与解释
1
0 {}
true false
TypeError第二行说明私有字段不在属性列表里,序列化也带不出去。最后一段传进去的对象虽然有个叫 '#count' 的字符串键,但那只是普通属性,和私有字段不是一回事,读取时引擎发现这个对象没有 Counter 的私有字段,直接抛 TypeError。
它凭什么是真正的私有
私有字段靠的是词法作用域,也就是「代码写在哪里」。#count 这个名字只在 Counter 的类体里有效,出了这对花括号就不存在,所以类外面写 obj.#count 属于引用了一个没声明的名字,解析器直接报错。这和变量作用域很像:函数里的局部变量,外面怎么都拿不到。
它也不是属性。每个实例在构造时,引擎会在对象内部单独记一份私有数据,和普通属性表分开存放。所以所有基于属性的反射手段,比如 Object.getOwnPropertyNames、Object.getOwnPropertyDescriptors,都看不到它,delete 也删不掉它。
私有方法和静态私有成员也是同一套规则:#format() {} 是私有方法,static #instances = 0 是静态私有字段,一样只能在类体里用。
和 TypeScript private、闭包比有什么不同
TypeScript 的 private 只在编译时检查,编译成 JavaScript 后就是普通属性,obj['count'] 照样能读到,console.log 也能打印出来。# 是运行时的保证,编译前后都拿不到。
闭包方案是在构造函数里定义局部变量,再把方法挂到实例上。它也能做到真正私有,但每个实例都要各自创建一份方法,没法放到原型上共享。私有字段的方法依然在原型上,内存更省,写法也更像普通类。
容易答错的地方
- 以为子类能访问父类的
#字段 - 不能。子类的类体是另一个作用域,写
this.#count会报语法错误。父类想让子类用,只能提供普通方法或 getter。 - 以为
#count和this['#count']是同一个东西 this['#count']是一个名字叫'#count'的普通字符串属性,任何人都能读写。私有字段不能用方括号动态访问,只能写this.#count。- 以为
Proxy包一层不影响私有字段 Proxy包住实例后,方法里的this指向代理对象,而代理对象身上没有私有字段,this.#count会抛TypeError。Vue 3 的响应式对象包住带私有字段的类实例时就会踩到这个坑。
面试官还会怎么问?
怎么判断一个对象是不是某个类创建的?
instanceof 可以被 Symbol.hasInstance 或改原型骗过去,更可靠的是在类里写一个静态方法,用 #field in obj 判断。只有真正经过这个类构造的对象才会有这个私有字段。
同一个类的两个实例能互相读私有字段吗?
能。比如 equals(other) { return this.#count === other.#count } 完全合法。访问权限按类的代码划分,只要代码写在类体里,就能读任何本类实例的私有字段。
私有字段没声明直接在构造函数里 this.#x = 1 可以吗?
不行,会报语法错误。私有字段必须先在类体顶层声明,这一点和普通字段不同,普通属性可以随时 this.x = 1 动态添加。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。