TypeScript 中的 private 关键字和 private 字段有什么区别?

在 TypeScript 3.8 + 中,使用 private关键字标记成员 private 有什么不同:

class PrivateKeywordClass {
private value = 1;
}

使用 #私有字段 建议用于 JavaScript:

class PrivateFieldClass {
#value = 1;
}

我应该选择其中一个吗?

16302 次浏览

私人关键字

TypeScript 中的 私人关键字编译时注释。它告诉编译器一个属性只能在该类中访问:

class PrivateKeywordClass {
private value = 1;
}


const obj = new PrivateKeywordClass();
obj.value // compiler error: Property 'value' is private and only accessible within class 'PrivateKeywordClass'.

然而,可以很容易地绕过编译时检查,例如抛弃类型信息:

const obj = new PrivateKeywordClass();
(obj as any).value // no compile error

在运行时也不强制使用 private 关键字

发出的 JavaScript

在将 TypeScript 编译为 JavaScript 时,只需删除 private关键字:

class PrivateKeywordClass {
private value = 1;
}

成为:

class PrivateKeywordClass {
constructor() {
this.value = 1;
}
}

由此,您可以看到为什么 private关键字不提供任何运行时保护: 在生成的 JavaScript 中,它只是一个普通的 JavaScript 属性。

私人领域

私有字段 确保属性保持私有 在运行时:

class PrivateFieldClass {
#value = 1;


getValue() { return this.#value; }
}


const obj = new PrivateFieldClass();


// You can't access '#value' outside of class like this
obj.value === undefined // This is not the field you are looking for.
obj.getValue() === 1 // But the class itself can access the private field!


// Meanwhile, using a private field outside a class is a runtime syntax error:
obj.#value


// While trying to access the private fields of another class is
// a runtime type error:
class Other {
#value;


getValue(obj) {
return obj.#value // TypeError: Read of private field #value from an object which did not contain the field
}
}


new Other().getValue(new PrivateKeywordClass());

如果尝试在类之外使用私有字段,TypeScript 还会输出编译时错误:

Error on accessing a private field

私有字段来源于 TC-39 ECMAScript 提案,是2021 ECMAScript 规范的一部分,这意味着它们可以在普通的 JavaScript 和 TypeScript 中使用。

发出的 JavaScript

如果您在 TypeScript 中使用私有字段,并将 ES2021或更老版本的 JavaScript 作为输出目标,TypeScript 将生成使用 WeakMap(来源)模拟私有字段运行时行为的代码

class PrivateFieldClass {
constructor() {
_x.set(this, 1);
}
}
_x = new WeakMap();

如果您的目标是 ES2021之后的任何内容,TypeScript 将发出私有字段:

class PrivateFieldClass {
constructor() {
this.#x = 1;
}
#x;
}

我该用哪个?

这取决于你想要达到什么样的目标。

private关键字是一个很好的默认值。它完成了设计的目的,并且已经被 TypeScript 开发人员成功地使用了多年。如果有现有的代码库,则不需要切换所有代码来使用私有字段。如果您的目标不是 esnext,那么情况尤其如此,因为 TS 为私有字段发出的 JS 可能会对性能产生影响。还要记住,私有字段与 private关键字还有其他细微但重要的区别

但是,如果需要强制执行运行时私有性,或者正在输出 esnext JavaScript,则应该使用私有字段。

还要记住,随着私有字段在 JavaScript/TypeScript 生态系统中变得越来越普遍,组织/社区关于使用其中一个或另一个的约定也会发生变化

其他值得注意的不同之处

  • Object.getOwnPropertyNames和类似的方法不返回私有字段

  • 私有字段不由 JSON.stringify序列化

  • 关于继承有一些重要的边缘情况。

    例如,TypeScript 禁止在子类中声明与超类中的私有属性同名的私有属性。

    class Base {
    private value = 1;
    }
    
    
    class Sub extends Base {
    private value = 2; // Compile error:
    }
    

    对于私有字段,情况并非如此:

    class Base {
    #value = 1;
    }
    
    
    class Sub extends Base {
    #value = 2; // Not an error
    }
    
  • 没有初始值设定项的 private关键字 private 属性不会在发出的 JavaScript 中生成属性声明:

    class PrivateKeywordClass {
    private value?: string;
    getValue() { return this.value; }
    }
    

    汇编至:

    class PrivateKeywordClass {
    getValue() { return this.value; }
    }
    

    而私有字段总是生成一个属性声明:

    class PrivateKeywordClass {
    #value?: string;
    getValue() { return this.#value; }
    }
    

    编译到(当目标为 esnext时) :

    class PrivateKeywordClass {
    #value;
    getValue() { return this.#value; }
    }
    

进一步阅读:

用例: #-私有字段

前言:

编译时 还有运行时隐私

私有字段提供编译时 还有运行时隐私,这是不“可破解的”。它是一种防止从类主体 以任何直接的方式外部访问成员的机制。

class A {
#a: number;
constructor(a: number) {
this.#a = a;
}
}


let foo: A = new A(42);
foo.#a; // error, not allowed outside class bodies
(foo as any).#bar; // still nope.

安全类继承

私有字段获得一个唯一的作用域。类层次结构可以实现,而不会意外地覆盖具有相同名称的私有属性。

class A {
#a = "a";
fnA() { return this.#a; }
}


class B extends A {
#a = "b";
fnB() { return this.#a; }
}


const b = new B();
b.fnA(); // returns "a" ; unique property #a in A is still retained
b.fnB(); // returns "b"

幸运的是,当 private属性有被覆盖的危险时,TS 编译器会发生错误(请参见 这个例子)。但是由于编译时特性的性质,在运行时一切都是可能的,因此忽略编译错误和/或利用发出的 JS 代码。

外部图书馆

库作者可以重构 #私有标识符,而不会对客户端造成破坏性更改。另一方的库用户受到保护,不能访问内部字段。

JSAPI 省略 #-private 字段

内置的 JS 函数和方法忽略 #私有字段。这可以在运行时导致更可预测的属性选择。例如: Object.keysObject.entriesJSON.stringifyfor..in循环和其他(代码示例; 参见 Matt Bierner 的 回答) :

class Foo {
#bar = 42;
baz = "huhu";
}


Object.keys(new Foo()); // [ "baz" ]

用例: private关键字

前言:

访问内部类 API 和状态(仅在编译时保密)

类的 private成员是运行时的常规属性。我们可以使用这种灵活性从外部访问类的内部 API 或状态。为了满足编译器检查,可以使用类型断言、动态属性访问或 @ts-ignore等机制。

类型断言(as/<>)和 any类型化变量赋值的示例:

class A {
constructor(private a: number) { }
}


const a = new A(10);
a.a; // TS compile error
(a as any).a; // works
const casted: any = a; casted.a // works

TS 甚至允许具有 逃生舱口private成员的动态属性访问:

class C {
private foo = 10;
}


const res = new C()["foo"]; // 10, res has type number

私人访问在哪里有意义?(1)单元测试,(2)调试/日志记录情况,或(3)其他具有项目内部类的高级案例场景(开放式列表)。

访问内部变量是有点矛盾的-否则你不会把它们放在第一位的 private。举个例子,单元测试应该是黑色/灰色的盒子,隐藏了私有字段作为实现细节。然而在实践中,可能存在从一个案例到另一个案例的有效方法。

适用于所有 ES 环境

TS private修饰符可用于所有 ES 目标。私有字段仅适用于 target ES2015/ES6或更高版本。在 ES6 + 中,WeakMap在内部用作下层实现(参见 给你)。本机 #-私有字段目前需要 target esnext

一致性和兼容性

团队可以使用编码指南和 linter 规则来强制使用 private作为唯一的访问修饰符。这种限制有助于保持一致性,并避免以向后兼容的方式与 #-private 字段符号混淆。

如果需要,参数属性(构造函数分配的简写)是一个显示终止符。它们只能与 private关键字一起使用,而且 没有计划还没有为 #-private 字段实现它们。

其他原因

  • private可能在一些低级别的情况下提供更好的运行时性能(参见 给你)。
  • 到目前为止,TS 中还没有可用的硬私有类方法。
  • 有些人更喜欢 private关键字符号。

两个都要注意

这两种方法都会在编译时创建某种名义类型或品牌类型。

class A1 { private a = 0; }
class A2 { private a = 42; }


const a: A1 = new A2();
// error: "separate declarations of a private property 'a'"
// same with hard private fields

此外,两者都允许跨实例访问: 类 A的实例可以访问其他 A实例的私有成员:

class A {
private a = 0;
method(arg: A) {
console.log(arg.a); // works
}
}

消息来源