私有属性
私有属性是常规类属性(包括类字段、类方法等)的对应项,这些属性是公开的。私有属性通过使用哈希 #
前缀创建,并且不能在类外部合法地引用。这些类属性的隐私封装由 JavaScript 本身强制执行。访问私有属性的唯一方法是通过点表示法,并且只能在定义私有属性的类中执行此操作。
在该语法存在之前,私有属性不是语言的原生特性。在原型继承中,可以使用WeakMap
对象或闭包模拟其行为,但它们在人体工程学方面无法与 #
语法相媲美。
语法
class ClassWithPrivate {
#privateField;
#privateFieldWithInitializer = 42;
#privateMethod() {
// …
}
static #privateStaticField;
static #privateStaticFieldWithInitializer = 42;
static #privateStaticMethod() {
// …
}
}
还有一些额外的语法限制
- 在类中声明的所有私有标识符必须是唯一的。命名空间在静态属性和实例属性之间共享。唯一的例外是当两个声明定义 getter-setter 对时。
- 私有标识符不能是
#constructor
。
描述
大多数类属性都有其私有对应项
- 私有字段
- 私有方法
- 私有静态字段
- 私有静态方法
- 私有 getter
- 私有 setter
- 私有静态 getter
- 私有静态 setter
这些特性统称为私有属性。但是,构造函数在 JavaScript 中不能是私有的。为了防止在类外部构造类,您必须使用私有标志。
私有属性用# 名称(发音为“哈希名称”)声明,这些名称是带有 #
前缀的标识符。哈希前缀是属性名称的固有部分——您可以将其与旧的下划线前缀约定 _privateField
联系起来——但它不是普通的字符串属性,因此您不能使用方括号表示法动态访问它。
从类外部引用 #
名称是语法错误。引用在类主体中未声明的私有属性或尝试使用delete
删除已声明的属性也是语法错误。
class ClassWithPrivateField {
#privateField;
constructor() {
delete this.#privateField; // Syntax error
this.#undeclaredField = 42; // Syntax error
}
}
const instance = new ClassWithPrivateField();
instance.#privateField; // Syntax error
JavaScript 作为一种动态语言,能够执行此编译时检查,因为特殊的哈希标识符语法使其在语法级别上与普通属性不同。
注意:在 Chrome 控制台中运行的代码可以在类外部访问私有属性。这是 JavaScript 语法限制的 DevTools 独有放松。
如果从没有该属性的对象访问私有属性,则会抛出TypeError
,而不是像普通属性那样返回 undefined
。
class C {
#x;
static getX(obj) {
return obj.#x;
}
}
console.log(C.getX(new C())); // undefined
console.log(C.getX({})); // TypeError: Cannot read private member #x from an object whose class did not declare it
此示例还说明您也可以在静态函数中以及在类的外部定义的实例上访问私有属性。
您可以使用in
运算符检查外部定义的对象是否拥有私有属性。如果私有字段或方法存在,则返回 true
,否则返回 false
。
class C {
#x;
constructor(x) {
this.#x = x;
}
static getX(obj) {
if (#x in obj) return obj.#x;
return "obj must be an instance of C";
}
}
console.log(C.getX(new C("foo"))); // "foo"
console.log(C.getX(new C(0.196))); // 0.196
console.log(C.getX(new C(new Date()))); // the current date and time
console.log(C.getX({})); // "obj must be an instance of C"
请注意私有名称始终预先声明且不可删除的推论:如果您发现对象拥有当前类的某个私有属性(来自 try...catch
或 in
检查),则它必须拥有所有其他私有属性。拥有类私有属性的对象通常表示它是由该类构造的(尽管并非总是如此)。
私有属性不是原型继承模型的一部分,因为它们只能在当前类的主体中访问,并且不会被子类继承。不同类中名称相同的私有属性完全不同,并且不会相互交互。将它们视为附加到每个实例的外部元数据,由类管理。因此,structuredClone()
不会克隆私有属性,并且Object.freeze()
和Object.seal()
对私有属性没有影响。
有关如何以及何时初始化私有字段的更多信息,请参阅公共类字段。
示例
私有字段
私有字段包括私有实例字段和私有静态字段。私有字段只能从类声明内部访问。
私有实例字段
与它们的公共对应项一样,私有实例字段
- 在基类中运行构造函数之前添加,或者在子类中调用
super()
之后立即添加,并且 - 仅在类的实例上可用。
class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
}
}
class Subclass extends ClassWithPrivateField {
#subPrivateField;
constructor() {
super();
this.#subPrivateField = 23;
}
}
new Subclass(); // In some dev tools, it shows Subclass {#privateField: 42, #subPrivateField: 23}
注意:来自 ClassWithPrivateField
基类的 #privateField
对 ClassWithPrivateField
是私有的,并且无法从派生的 Subclass
访问。
返回覆盖对象
类的构造函数可以返回一个不同的对象,该对象将用作派生类构造函数的新 this
。然后,派生类可以在该返回的对象上定义私有字段——这意味着可以将私有字段“标记”到不相关的对象上。
class Stamper extends class {
// A base class whose constructor returns the object it's given
constructor(obj) {
return obj;
}
} {
// This declaration will "stamp" the private field onto the object
// returned by the base class constructor
#stamp = 42;
static getStamp(obj) {
return obj.#stamp;
}
}
const obj = {};
new Stamper(obj);
// `Stamper` calls `Base`, which returns `obj`, so `obj` is
// now the `this` value. `Stamper` then defines `#stamp` on `obj`
console.log(obj); // In some dev tools, it shows {#stamp: 42}
console.log(Stamper.getStamp(obj)); // 42
console.log(obj instanceof Stamper); // false
// You cannot stamp private properties twice
new Stamper(obj); // Error: Initializing an object twice is an error with private fields
警告:这可能是一件非常令人困惑的事情。通常建议您避免从构造函数中返回任何内容——尤其是与 this
不相关的内容。
私有静态字段
与它们的公共对应项一样,私有静态字段
- 在类评估时添加到类构造函数中,并且
- 仅在类本身可用。
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
return ClassWithPrivateStaticField.#privateStaticField;
}
}
console.log(ClassWithPrivateStaticField.publicStaticMethod()); // 42
私有静态字段有一个限制:只有定义私有静态字段的类才能访问该字段。在使用this
时,这可能会导致意外行为。在以下示例中,当我们尝试调用 Subclass.publicStaticMethod()
时,this
指的是 Subclass
类(而不是 ClassWithPrivateStaticField
类),因此会导致 TypeError
。
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
return this.#privateStaticField;
}
}
class Subclass extends ClassWithPrivateStaticField {}
Subclass.publicStaticMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it
如果您使用 super
调用该方法,则情况相同,因为super
方法不是以超类作为 this
调用的。
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
// When invoked through super, `this` still refers to Subclass
return this.#privateStaticField;
}
}
class Subclass extends ClassWithPrivateStaticField {
static callSuperMethod() {
return super.publicStaticMethod();
}
}
Subclass.callSuperMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it
建议您始终通过类名访问私有静态字段,而不是通过 this
访问,这样继承就不会破坏该方法。
私有方法
私有方法包括私有实例方法和私有静态方法。私有方法只能从类声明内部访问。
私有实例方法
与它们的公共对应项不同,私有实例方法
- 在安装实例字段之前立即安装,并且
- 仅在类的实例上可用,而不是在其
.prototype
属性上可用。
class ClassWithPrivateMethod {
#privateMethod() {
return 42;
}
publicMethod() {
return this.#privateMethod();
}
}
const instance = new ClassWithPrivateMethod();
console.log(instance.publicMethod()); // 42
私有实例方法可以是生成器、异步或异步生成器函数。私有 getter 和 setter 也是可能的,并且遵循与其公共 getter 和 setter 对应部分相同的语法要求。
class ClassWithPrivateAccessor {
#message;
get #decoratedMessage() {
return `🎬${this.#message}🛑`;
}
set #decoratedMessage(msg) {
this.#message = msg;
}
constructor() {
this.#decoratedMessage = "hello world";
console.log(this.#decoratedMessage);
}
}
new ClassWithPrivateAccessor(); // 🎬hello world🛑
与公共方法不同,私有方法无法在类的 .prototype
属性上访问。
class C {
#method() {}
static getMethod(x) {
return x.#method;
}
}
console.log(C.getMethod(new C())); // [Function: #method]
console.log(C.getMethod(C.prototype)); // TypeError: Receiver must be an instance of class C
私有静态方法
与它们的公共对应部分一样,私有静态方法
- 在类评估时添加到类构造函数中,并且
- 仅在类本身可用。
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod() {
return ClassWithPrivateStaticMethod.#privateStaticMethod();
}
}
console.log(ClassWithPrivateStaticMethod.publicStaticMethod()); // 42
私有静态方法可以是生成器、异步和异步生成器函数。
之前提到的私有静态字段的相同限制也适用于私有静态方法,并且类似地,在使用 this
时可能导致意外行为。在以下示例中,当我们尝试调用 Subclass.publicStaticMethod()
时,this
指的是 Subclass
类(而不是 ClassWithPrivateStaticMethod
类),因此会导致 TypeError
。
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod() {
return this.#privateStaticMethod();
}
}
class Subclass extends ClassWithPrivateStaticMethod {}
console.log(Subclass.publicStaticMethod()); // TypeError: Cannot read private member #privateStaticMethod from an object whose class did not declare it
模拟私有构造函数
许多其他语言都包含将构造函数标记为私有的功能,这可以防止类在类本身之外被实例化——你只能使用创建实例的静态工厂方法,或者根本无法创建实例。JavaScript 没有原生方法来做到这一点,但可以通过使用私有静态标志来实现。
class PrivateConstructor {
static #isInternalConstructing = false;
constructor() {
if (!PrivateConstructor.#isInternalConstructing) {
throw new TypeError("PrivateConstructor is not constructable");
}
PrivateConstructor.#isInternalConstructing = false;
// More initialization logic
}
static create() {
PrivateConstructor.#isInternalConstructing = true;
const instance = new PrivateConstructor();
return instance;
}
}
new PrivateConstructor(); // TypeError: PrivateConstructor is not constructable
PrivateConstructor.create(); // PrivateConstructor {}
规范
规范 |
---|
ECMAScript 语言规范 # prod-PrivateIdentifier |
ECMAScript 语言规范 # prod-00OK517S |
浏览器兼容性
javascript.classes.private_class_fields
BCD 表格仅在浏览器中加载
javascript.classes.private_class_fields_in
BCD 表格仅在浏览器中加载
javascript.classes.private_class_methods
BCD 表格仅在浏览器中加载
另请参阅
- 使用类 指南
- 类
- 公共类字段
class
- TC39 类字段提案中的私有语法常见问题解答
- 所有 JS 类元素的语义,作者:郭舒羽 (2018)
- v8.dev 上的公共和私有类字段 (2018)