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

What are the differences between the private keyword and private fields in TypeScript?

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

class PrivateKeywordClass {
    private value = 1;
}

并使用 # 私有字段 proposed for JavaScript:

class PrivateFieldClass {
    #value = 1;
}

我应该更喜欢一个吗?

私人关键字

TypeScript 中的 private keyword 是一个 编译时 注释。它告诉编译器 属性 应该只能在 class 内部访问:

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 属性。

私人领域

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());

如果您尝试在 class:

之外使用私有字段,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,那么您应该使用私有字段。

另请记住,organization/community 关于使用其中一个或另一个的约定也会随着私有领域在 JavaScript/TypeScript 生态系统中变得更加广泛而发展

其他注意事项

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

  • 私有字段没有被JSON.stringify

  • 序列化
  • 关于继承有一些重要的边缘案例。

    例如,TypeScript 禁止在 subclass 中声明私有 属性,其名称与 superclass.[=43= 中的私有 属性 同名]

    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; }
    }
    

延伸阅读:

用例:#-私有字段

前言:

编译时运行-时间隐私

#-私有字段提供编译时运行-时间隐私,这不是"hackable"。这是一种防止从 class 正文 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.

安全class继承

#-私有字段获得唯一范围。 Class 可以实现层次结构而不会意外覆盖具有相同名称的私有属性。

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)。但由于编译时特性的性质,在 运行 时一切仍然可能,考虑到编译错误被忽略 and/or 使用发出的 JS 代码。

外部图书馆

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

JS API省略#-私有字段

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

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

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

用例:private 关键字

前言:

访问内部 class API 和状态(仅编译时隐私)

private class 的成员是 运行 时间的常规属性。我们可以利用这种灵活性从外部访问 class 内部 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) debugging/logging 情况或 (3) 其他具有项目内部 classes 的高级案例场景(开放式列表)。

对内部变量的访问有点矛盾——否则你一开始就不会private。举个例子,单元测试应该是 black/grey 框,其中隐藏了私有字段作为实现细节。但在实践中,根据具体情况可能会有有效的方法。

在所有 ES 环境中可用

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

一致性和兼容性

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

如果需要,parameter properties (constructor assignment shorthand) are a show stopper. They can only be used with private keyword and there are no plans 尚未为 #-私有字段实施它们。

其他原因

  • private 在某些降级情况下可能会提供更好的 运行 时间性能(参见 here)。
  • 到目前为止,TS 中没有硬私有 class 方法。
  • 有些人更喜欢 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

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

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

来源