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

Posted

技术标签:

【中文标题】TypeScript 中的 private 关键字和 private 字段有啥区别?【英文标题】:What are the differences between the private keyword and private fields in TypeScript?TypeScript 中的 private 关键字和 private 字段有什么区别? 【发布时间】:2020-04-25 17:39:44 【问题描述】:

在 TypeScript 3.8+ 中,使用 private 关键字将成员标记为私有有什么区别:

class PrivateKeywordClass 
    private value = 1;

并使用# 私有字段proposed for javascript

class PrivateFieldClass 
    #value = 1;

我应该更喜欢一个吗?

【问题讨论】:

The future of the "private" keyword in TypeScript. 【参考方案1】:

私人关键字

TypeScript 中的private keyword 是一个编译时注解。它告诉编译器一个属性只能在该类中访问:

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

privatekeyword 也不会在运行时强制执行

发出的 JavaScript

将 TypeScript 编译为 JavaScript 时,private 关键字被简单地删除:

class PrivateKeywordClass 
    private value = 1;

变成:

class PrivateKeywordClass 
    constructor() 
        this.value = 1;
    

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

私人领域

Private fields 确保属性在运行时保持私有

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 也会输出编译时错误:

私有字段来自JavaScript proposal,也可以在普通 JavaScript 中使用。

发出的 JavaScript

如果您在 TypeScript 中使用私有字段并且输出的目标是旧版本的 JavaScript,例如 es6es2018,TypeScript 将尝试生成模拟私有字段的运行时行为的代码

class PrivateFieldClass 
    constructor() 
        _x.set(this, 1);
    

_x = new WeakMap();

如果你的目标是 esnext,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 关键字私有属性将不会在发出的 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; 

延伸阅读:

The future of the "private" keyword TypeScript PR that added private fields

【讨论】:

【参考方案2】:

用例:#-私有字段

前言:

TC39 proposal class-fields 同义词:#-private、硬私有、运行时私有

编译时运行时隐私

#-private 字段提供编译时运行时隐私,这是不可“破解”的。这是一种防止从类体in any direct way之外访问成员的机制。

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.

安全类继承

#-private 字段具有唯一的范围。可以在不意外覆盖同名私有属性的情况下实现类层次结构。

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 编译器会发出错误(请参阅this example)。但是由于编译时特性的性质,在运行时一切仍然是可能的,因为编译错误被忽略和/或发出的 JS 代码被利用。

外部库

库作者可以重构#-private 标识符,而不会对客户端造成重大更改。另一边的图书馆用户受到保护,无法访问内部字段。

JS API 省略了#-private 字段

内置的 JS 函数和方法忽略 #-private 字段。这可以在运行时产生更可预测的属性选择。示例:Object.keysObject.entriesJSON.stringifyfor..in 循环和其他(code sample;另见 Matt Bierner 的 answer):

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


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

用例:private关键字

前言:

private keyword in TS docs 同义词:TS 私有、软私有、编译时私有

访问内部类 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 甚至允许使用escape-hatch 对private 成员进行动态属性访问:

class C 
  private foo = 10;


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

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

访问内部变量有点矛盾——否则你一开始就不会把它们设为private。举个例子,单元测试应该是黑色/灰色的盒子,私有字段隐藏为实现细节。但在实践中,可能存在针对不同情况的有效方法。

适用于所有 ES 环境

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

一致性和兼容性

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

如果需要,parameter properties(构造函数分配简写)是一个显示停止器。它们只能与private 关键字一起使用,并且no plans 尚未为#-private 字段实现它们。

其他原因

private 可能在某些降级情况下提供更好的运行时性能(请参阅here)。 到目前为止,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
    


来源

https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#ecmascript-private-fields https://github.com/microsoft/TypeScript/issues/31670 https://github.com/Microsoft/TypeScript/pull/30829

【讨论】:

以上是关于TypeScript 中的 private 关键字和 private 字段有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript 构造函数中的私有变量声明以诱导 DI

TypeScript -- 面向对象特性

TypeScript中的privateprotected

TypeScript面向对象

TypeScript面向对象

在 TypeScript 中获取和设置