likes
comments
collection
share

Typescript:不能依赖修饰符private来隐藏信息

作者站长头像
站长
· 阅读数 3

一直以来,JavaScript都缺乏一种使类的属性成为私有的方法。通常的变通方法是将下划线作为不属于公共API的字段的前缀:

class Foo {
    _private = "secret123";
}

但这只是建议用户不要访问私人数据,而它很容易被绕开:

const f = new Foo();
f._private; // "secret123"

TypeScript增加的public、protected和private访问修饰符,似乎提供了一些强制执行的功能:

class Diary {
    private secret = "cheated on my English test";
}

const diary = new Diary();
diary.secret
// ---- 属性 "secret"为私有属性,只能在类 "Diary"中访问

但是private是类型系统的一个特性,而且和类型系统中的所有特性一样,它在运行时就不见了,下面是这段代码在Typescript中编译成JavaScript时的样子(用target=ES2017):

class Diary {
    constructor () {
        this.secret = "cheated on my English test";
    }
}

const diary = new Diary();
diary.secret;

private修饰符消失了,你的秘密(secret)也就暴露了!就像_private惯例一样,Typescript的访问修饰符只会阻止你访问私有数据。通过类型断言,你甚至可以从Typescript内部访问一个私有属性:

class Person {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const p = new Person('Alice');
console.log(p.name); // error: Property 'name' is private and only accessible within class 'Person'.
(p as any).name = 'Bob'; // no error, but this is not type safe and should be avoided
p.greet(); // output: Hello, my name is Bob

在上面的代码中,虽然使用了private修饰符将name成员设为私有的,但是在类的外部,仍然可以通过类型断言将p的类型转换为any类型,然后访问和修改name成员。这显然是不安全的。

因此,尽管private修饰符提供了一定程度上的封装性和安全性,但是仍需要开发者自觉遵守规则,避免不必要的类型断言和其他方式的访问和修改。

换句话说,不能依赖私有访问操作符private来隐藏信息!

那么如果真的要来点强有力的方法的话,应该要怎么做?传统的答案是利用JavaScript可靠的隐藏信息的方法之一--闭包。先看下下面的代码:

declare function hash(text: string): number;

class PasswordChecker {
    checkPassword: (password: string) => boolean;
    constructor (passwordHash: number) {
        this.checkPassword = (password: string) => {
            return has(password) === passwordHash;
        }
    }
}

const checker = new PasswordChecker(hash("s3cret"));
checker.checkPassword("s3cret"); // 返回 true

JavaScript没有提供从PasswordChecker的构造函数之外访问 passwordHash 变量的方法,但是这确实也有一些缺点:特别是,因为passwordHash在构造函数之外是不可见的,所以每个使用它的方法也必须在那里定义。这就导致每个类的实例都要创建一个方法的副本,从而导致更高的内存使用量。它还会阻止同一类的其他实例访问私有数据。闭包可能是不方便的,但它肯定会保证你数据的私有性!

一个新的选择是使用私有字段,从 TypeScript 3.8 开始,引入了一种新的语法糖 # 来支持私有字段。在类中,通过在变量名前添加 # 符号来表示这是一个私有字段,外部无法直接访问该字段。以下是一个示例:

class MyClass {
  #privateField: string;

  constructor(privateField: string) {
    this.#privateField = privateField;
  }

  public publicMethod() {
    console.log(this.#privateField);
  }
}

const instance = new MyClass("private value");
instance.publicMethod(); // 输出 "private value"
console.log(instance.#privateField); // 语法错误,无法访问私有字段

使用 # 可以更加清晰地表明变量是私有的,而且可以确保外部无法直接访问私有字段。但需要注意的是,这种语法仍然需要编译成 JavaScript 才能运行,而且一些老旧的浏览器或 Node.js 版本可能不支持该语法。

最后,如果你担心的是安全问题,而不仅仅是封装问题,那么还有其他需要注意的问题,比如对内置原型(prototype)和函数的修改。

需要记住的事情

  • private 访问修饰符只能通过类型系统才能被强制执行。它在运行时没有效果,可以被一个类型断言轻松绕过。不要以为它还能保持数据的隐藏性。
  • 为了更可靠的信息隐藏,请使用闭包。