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

Mat*_*ner 46 encapsulation private class typescript class-fields

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

class PrivateKeywordClass {
    private value = 1;
}
Run Code Online (Sandbox Code Playgroud)

并使用为 JavaScript 提议#私有字段:

class PrivateFieldClass {
    #value = 1;
}
Run Code Online (Sandbox Code Playgroud)

我应该更喜欢一个吗?

Mat*_*ner 67

私有关键字

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

class PrivateKeywordClass {
    private value = 1;
}

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

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

const obj = new PrivateKeywordClass();
(obj as any).value // no compile error
Run Code Online (Sandbox Code Playgroud)

private关键字也不会在运行时强制执行

发出的 JavaScript

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

class PrivateKeywordClass {
    private value = 1;
}
Run Code Online (Sandbox Code Playgroud)

变成:

class PrivateKeywordClass {
    private value = 1;
}

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

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

私有字段

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

const obj = new PrivateKeywordClass();
(obj as any).value // no compile error
Run Code Online (Sandbox Code Playgroud)

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

访问私有字段时出错

私有字段来自JavaScript 提案,也适用于普通 JavaScript。

发出的 JavaScript

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

class PrivateKeywordClass {
    private value = 1;
}
Run Code Online (Sandbox Code Playgroud)

如果您的目标是esnext,TypeScript 将发出私有字段:

class PrivateKeywordClass {
    constructor() {
        this.value = 1;
    }
}
Run Code Online (Sandbox Code Playgroud)

我应该使用哪一种?

这取决于您要实现的目标。

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

但是,如果您需要强制执行运行时隐私或输出esnextJavaScript,那么您应该使用私有字段。

另请记住,随着私有字段在 JavaScript/TypeScript 生态系统中变得更加普遍,使用其中一种或另一种的组织/社区约定也将发展

其他注意事项

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

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

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

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

    class Base {
        private value = 1;
    }
    
    class Sub extends Base {
        private value = 2; // Compile error:
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

    class Base {
        #value = 1;
    }
    
    class Sub extends Base {
        #value = 2; // Not an error
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 一个private没有初始化的关键字私有财产不会产生在发出JavaScript中的财产申报:

    class PrivateKeywordClass {
        private value?: string;
        getValue() { return this.value; }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    编译为:

    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());
    
    Run Code Online (Sandbox Code Playgroud)

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

    class PrivateKeywordClass {
        #value?: string;
        getValue() { return this.#value; }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    编译为(定位时esnext):

    class PrivateFieldClass {
        constructor() {
            _x.set(this, 1);
        }
    }
    _x = new WeakMap();
    
    Run Code Online (Sandbox Code Playgroud)

进一步阅读:


for*_*d04 8

用例:#-私有字段

前言:

编译时运行时隐私

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

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.
Run Code Online (Sandbox Code Playgroud)

安全的类继承

#-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"
Run Code Online (Sandbox Code Playgroud)

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

外部库

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

JS API 省略#-private 字段

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

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

Object.keys(new Foo()); // [ "baz" ]
Run Code Online (Sandbox Code Playgroud)

用例: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
Run Code Online (Sandbox Code Playgroud)

TS 甚至允许private使用escape-hatch对成员进行动态属性访问:

class C {
  private foo = 10;
}

const res = new C()["foo"]; // 10, res has type number
Run Code Online (Sandbox Code Playgroud)

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

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

适用于所有 ES 环境

TSprivate修饰符可用于所有 ES 目标。#-private 字段仅适用于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
Run Code Online (Sandbox Code Playgroud)

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

class A {
    private a = 0;
    method(arg: A) {
        console.log(arg.a); // works
    }
}
Run Code Online (Sandbox Code Playgroud)

来源