TypeScript小状况之对象的私有字段

2,851 阅读3分钟

前言

最近开始在复杂项目中使用TypeScript,然而在使用过程中遇到了一些状况和报错,决定开一个系列记录遇到的问题和我的解决方法。

private标识符

前几天在使用TypeScript写一个类的时候,看到可以声明private标识符,果断把不想暴露到外部的属性和方法加上,然而…………编译完之后发现在运行时的环境里面还是可以访问,跟我期望有点不太一样(我期望是类似Java那样真的访问不了)

TypeScript

class Foo {
  private a: number;
  private b: string;

  constructor () {
    this.a = 1;
    this.b = 'Tom';
  }

  setA (val: number) {
    this.a = val;
  }
  getA () {
    return this.a;
  }
  setB (val: string) {
    this.b = val;
  }
  getB () {
    return this.b;
  }
}

Webpack构建后的JavaScript

var Foo = (function () {
    function Foo() {
        this.a = 1;
        this.b = 'Tom';
    }
    Foo.prototype.setA = function (val) {
        this.a = val;
    };
    Foo.prototype.getA = function () {
        return this.a;
    };
    Foo.prototype.setB = function (val) {
        this.b = val;
    };
    Foo.prototype.getB = function () {
        return this.b;
    };
    return Foo;
}());

运行时结果

杀泼赖死~说好的private呢?

查询官方文档后发现有这么一段内容。

大致意思是,在TypeScript 3.8之后,TypeScript支持新的JavaScript的私有字段语法。而且这个私有字段语法是内置在JavaScript的运行时,能更好的确保每一个私有字段的相互隔离。而TypeScript的private只是在编译阶段声明某个字段是私有的。

于是,我们收获了新的私有字段语法#field~

ES6的私有字段语法

查看更多关于私有字段

那么我们把private标识符替换为#试试。

需要注意的是,把tsconfig.json里面的target改称es6。

"target": "es6",

TypeScript

class Foo {
  #a: number;
  #b: string;

  constructor () {
    this.#a = 1;
    this.#b = 'Tom';
  }

  setA (val: number) {
    this.#a = val;
  }
  getA () {
    return this.#a;
  }
  setB (val: string) {
    this.#b = val;
  }
  getB () {
    return this.#b;
  }
}

Webpack构建后的JavaScript

var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to set private field on non-instance");
    }
    privateMap.set(receiver, value);
    return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
    if (!privateMap.has(receiver)) {
        throw new TypeError("attempted to get private field on non-instance");
    }
    return privateMap.get(receiver);
};
var _a, _b;
class Foo {
    constructor() {
        _a.set(this, void 0);
        _b.set(this, void 0);
        __classPrivateFieldSet(this, _a, 1);
        __classPrivateFieldSet(this, _b, 'Tom');
    }
    setA(val) {
        __classPrivateFieldSet(this, _a, val);
    }
    getA() {
        return __classPrivateFieldGet(this, _a);
    }
    setB(val) {
        __classPrivateFieldSet(this, _b, val);
    }
    getB() {
        return __classPrivateFieldGet(this, _b);
    }
}
_a = new WeakMap(), _b = new WeakMap();

运行时结果

看到这里,细心的朋友会发现,有个不太常用(对我而言甚至是从没见过)的对象WeakMap出现了。究竟这个是什么呢?

WeakMap类型

查看更多关于WeakMap

按照官方的说法,WeakMap的诞生源于Map会一直持有每个键值(key-value)的引用,导致即使已经没有其他地方引用,内存也没有办法被回收,进而可能引发内存泄露。原生的WeakMap持有的是一个“弱引用”(weak reference),而这个弱引用的机制,会导致WeakMapMap有以下2个不同:

  1. 当且仅当键的引用没有被垃圾回收的时候,这个键值才是有效的。
  2. WeakMap的键不可被枚举,因为它的键列表收到垃圾回收的影响,没办法列出;所以如果需要一个可被枚举的键列表,应该使用Map

回到我们的例子中,经过webpack构建后的代码,实际上在ES5的环境下,声明了2个WeakMap类型的局部变量(_a_b)用于存放对应的私有字段,使用WeakMap的目的是防止内存泄漏。这两个局部变量利用JavaScript的闭包的机制,实现在getA()/setA()/getB()/setB()可被访问,而外部无法对私有字段进行直接访问。这也是在ES5/ES6以前,实现私有字段的常用手段。

私有方法

最后,有人可能会发现,私有字段只说了私有属性,那私有方法呢?

虽然TypeScript和ES6目前都不支持直接声明class的私有方法,但是我们可以用私有属性曲线救国。

// 不支持声明私有方法
class foo {
  #myFunc () {
    // blablabla
  }
}

// 曲线救国
class foo {
  #myFunc: Function;

  constructor () {
    this.#myFunc = () => {
      // blablabla
    }
  }
}