你不知道的 WeakMap

相信不少读者对 ES6 引入的 Map 已经不陌生了,其中的一部分读者可能也据说过 WeakMap。既生 Map 何生 WeakMap?带着这个问题,本文将围绕如下几个方面的内容为你详细介绍 WeakMap 的相关知识。javascript

you-dont-know-weakmap

1、什么是垃圾回收

在计算机科学中,垃圾回收(Garbage Collection,缩写为 GC)是指一种自动的存储器管理机制。当某个程序占用的一部份内存空间再也不被这个程序访问时,这个程序会借助垃圾回收算法向操做系统归还这部份内存空间。垃圾回收器能够减轻程序员的负担,也减小程序中的错误。html

垃圾回收最先起源于 LISP 语言,它有两个基本的原理:java

  • 考虑某个对象在将来的程序运行中,将不会被访问;
  • 回收这些对象所占用的存储器。

JavaScript 具备自动垃圾回收机制,这种垃圾回收机制原理其实很简单:找出那些再也不继续使用的变量,而后释放其所占用的内存,垃圾回收器会按照固定的时间间隔周期性地执行这一操做。node

gc-cycle

(图片来源:Garbage Collection: V8’s Orinoco)git

局部变量只有在函数执行的过程当中存在,在这个过程当中,通常状况下会为局部变量在栈内存上分配空间,而后在函数中使用这些变量,直至函数执行结束。垃圾回收器必须追踪每一个变量的使用状况,为那些再也不使用的变量打上标记,用于未来能及时回收其占用的内存,用于标识无用变量的策略主要有引用计数法和标记清除法。程序员

1.1 引用计数法

最先的也是最简单的垃圾回收实现方法,这种方法为占用物理空间的对象附加一个计数器,当有其余对象引用这个对象时计数器加一,反之引用解除时减一。这种算法会按期检查还没有被回收的对象的计数器,为零的话则回收其所占物理空间,由于此时的对象已经没法访问。es6

引用计数法实现比较简单,但它却没法回收循环引用的存储对象,好比:github

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1引用o2
  o2.p = o1; // o2引用o1
}

f();
复制代码

为了解决这个问题,垃圾回收器引入了标记清除法。算法

1.2 标记清除法

标记清除法主要将 GC 的垃圾回收过程分为标记阶段和清除两个阶段:typescript

  • 标记阶段:把全部活动对象作上标记;
  • 清除阶段:把没有标记(也就是非活动对象)销毁。

JavaScript 中最经常使用的垃圾回收方式就是标记清除(mark-and-sweep),当变量进入环境时,就将这个变量标记 “进入环境”,当变量离开环境时,就将其标记为 “离开环境”。

标记清除法具体的垃圾回收过程以下图所示:

gc_mark_sweep

(图片来源:How JavaScript works: memory management + how to handle 4 common memory leaks)

在平常工做中,对于再也不使用的对象,一般咱们会但愿它们会被垃圾回收器回收。这时,你可使用 null 来覆盖对应对象的引用,好比:

let sem = { name: "Semlinker" };
// 该对象能被访问,sem是它的引用
sem = null; // 覆盖引用
// 该对象将会被从内存中清除
复制代码

可是,当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是能够访问的。例如,若是把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即便没有其余对该对象的引用。好比:

let sem = { name: "Semlinker" };
let array = [ sem ];
sem = null; // 覆盖引用

// sem 被存储在数组里, 因此它不会被垃圾回收机制回收
// 咱们能够经过 array[0] 来获取它
复制代码

一样,若是咱们使用对象做为常规 Map 的键,那么当 Map 存在时,该对象也将存在。它会占用内存,而且不会被垃圾回收机制回收。好比:

let sem = { name: "Semlinker" };

let map = new Map();
map.set(sem, "全栈修仙之路");
sem = null; // 覆盖引用

// sem被存储在map中
// 咱们可使用map.keys()来获取它
复制代码

那么如何解决上述 Map 的垃圾回收问题呢?这时咱们就须要来了解一下 WeakMap。

2、为何须要 WeakMap

2.1 Map 和 WeakMap 的区别

相信不少读者对 ES6 中 Map 已经不陌生了,已经有了 Map,为何还会有 WeakMap,它们之间有什么区别呢?Map 和 WeakMap 之间的主要区别:

  • Map 对象的键能够是任何类型,但 WeakMap 对象中的键只能是对象引用;
  • WeakMap 不能包含无引用的对象,不然会被自动清除出集合(垃圾回收机制);
  • WeakMap 对象是不可枚举的,没法获取集合的大小。

在 JavaScript 里,Map API 能够经过使其四个 API 方法共用两个数组(一个存放键,一个存放值)来实现。给这种 Map 设置值时会同时将键和值添加到这两个数组的末尾。从而使得键和值的索引在两个数组中相对应。当从该 Map 取值的时候,须要遍历全部的键,而后使用索引从存储值的数组中检索出相应的值。

但这样的实现会有两个很大的缺点,首先赋值和搜索操做都是 O(n) 的时间复杂度(n 是键值对的个数),由于这两个操做都须要遍历所有整个数组来进行匹配。另一个缺点是可能会致使内存泄漏,由于数组会一直引用着每一个键和值。 这种引用使得垃圾回收算法不能回收处理他们,即便没有其余任何引用存在了。

相比之下,原生的 WeakMap 持有的是每一个键对象的 “弱引用”,这意味着在没有其余引用存在时垃圾回收能正确进行。 原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。

正因为这样的弱引用,WeakMap 的 key 是不可枚举的 (没有方法能给出全部的 key)。若是key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而获得不肯定的结果。所以,若是你想要这种类型对象的 key 值的列表,你应该使用 Map。而若是你要往对象上添加数据,又不想干扰垃圾回收机制,就可使用 WeakMap。

因此对于前面遇到的垃圾回收问题,咱们可使用 WeakMap 来解决,具体以下:

let sem = { name: "Semlinker" };

let map = new WeakMap();
map.set(sem, "全栈修仙之路");
sem = null; // 覆盖引用
复制代码

2.2 WeakMap 与垃圾回收

WeakMap 真有介绍的那么神奇么?下面咱们来动手测试一下同个场景下 Map 与 WeakMap 对垃圾回收的影响。首先咱们分别建立两个文件:map.js 和 weakmap.js。

map.js

//map.js
function usageSize() {
  const used = process.memoryUsage().heapUsed;
  return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}

global.gc();
console.log(usageSize()); // ≈ 3.19M

let arr = new Array(10 * 1024 * 1024);
const map = new Map();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.19M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 83.2M
复制代码

建立完 map.js 以后,在命令行输入 node --expose-gc map.js 命令执行 map.js 中的代码,其中 --expose-gc 参数表示容许手动执行垃圾回收机制。

weakmap.js

function usageSize() {
  const used = process.memoryUsage().heapUsed;
  return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}

global.gc();
console.log(usageSize()); // ≈ 3.19M

let arr = new Array(10 * 1024 * 1024);
const map = new WeakMap();

map.set(arr, 1);
global.gc();
console.log(usageSize()); // ≈ 83.2M

arr = null;
global.gc();
console.log(usageSize()); // ≈ 3.2M
复制代码

一样,建立完 weakmap.js 以后,在命令行输入 node --expose-gc weakmap.js 命令执行 weakmap.js 中的代码。经过对比 map.jsweakmap.js 的输出结果,咱们可知 weakmap.js 中定义的 arr 被清除后,其占用的堆内存被垃圾回收器成功回收了。

下面咱们来大体分析一下出现上述区别的主要缘由:

对于 map.js 来讲,因为在 arr 和 Map 中都保留了数组的强引用,因此在 Map 中简单的清除 arr 变量内存并无获得释放,由于 Map 还存在引用计数。而在 WeakMap 中,它的键是弱引用,不计入引用计数中,因此当 arr 被清除以后,数组会由于引用计数为 0 而被垃圾回收清除。

了解完上述内容以后,下面咱们来正式介绍 WeakMap。

3、WeakMap 简介

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。WeakMap 的 key 只能是 Object 类型。 原始数据类型是不能做为 key 的(好比 Symbol)。

3.1 语法

new WeakMap([iterable])
复制代码

iterable:是一个数组(二元数组)或者其余可迭代的且其元素是键值对的对象。每一个键值对会被加到新的 WeakMap 里。null 会被当作 undefined。

3.2 属性

  • length:属性的值为 0;
  • prototypeWeakMap 构造器的原型。 容许添加属性到全部的 WeakMap 对象。

3.3 方法

  • WeakMap.prototype.delete(key):移除 key 的关联对象。执行后 WeakMap.prototype.has(key) 返回false。
  • WeakMap.prototype.get(key):返回 key 关联对象,或者 undefined(没有 key 关联对象时)。
  • WeakMap.prototype.has(key):根据是否有 key 关联对象返回一个布尔值。
  • WeakMap.prototype.set(key, value):在 WeakMap 中设置一组 key 关联对象,返回这个 WeakMap 对象。

3.4 示例

const wm1 = new WeakMap(),
      wm2 = new WeakMap(),
      wm3 = new WeakMap();
const o1 = {},
      o2 = function(){},
      o3 = window;

wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // value能够是任意值,包括一个对象或一个函数
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值能够是任意对象,甚至另一个WeakMap对象

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined,wm2中没有o2这个键
wm2.get(o3); // undefined,值就是undefined

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即便值是undefined)

wm3.set(o1, 37);
wm3.get(o1); // 37

wm1.has(o1);   // true
wm1.delete(o1);
wm1.has(o1);   // false
复制代码

介绍完 WeakMap 相关的基础知识,下面咱们来介绍一下 WeakMap 的应用。

4、WeakMap 应用

4.1 经过 WeakMap 缓存计算结果

使用 WeakMap,你能够将先前计算的结果与对象相关联,而没必要担忧内存管理。如下功能 countOwnKeys() 是一个示例:它将之前的结果缓存在 WeakMap 中 cache

const cache = new WeakMap();

function countOwnKeys(obj) {
  if (cache.has(obj)) {
    return [cache.get(obj), 'cached'];
  } else {
    const count = Object.keys(obj).length;
    cache.set(obj, count);
    return [count, 'computed'];
  }
}
复制代码

建立完 countOwnKeys 方法,咱们来具体测试一下:

let obj = { name: "kakuqo", age: 30 };
console.log(countOwnKeys(obj));
// [2, 'computed']
console.log(countOwnKeys(obj));
// [2, 'cached']
obj = null; // 当对象不在使用时,设置为null
复制代码

4.2 在 WeakMap 中保留私有数据

在如下代码中,WeakMap _counter_action 用于存储如下实例的虚拟属性的值:

const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  
  dec() {
    let counter = _counter.get(this);
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}
复制代码

建立完 Countdown 类,咱们来具体测试一下:

let invoked = false;

const countDown = new Countdown(3, () => invoked = true);
countDown.dec();
countDown.dec();
countDown.dec();

console.log(`invoked status: ${invoked}`)
复制代码

说到类的私有属性,咱们不得提一下 ECMAScript Private Fields

5、ECMAScript 私有字段

5.1 ES 私有字段简介

在介绍 ECMAScript 私有字段前,咱们先目击一下它的 “芳容”:

class Counter extends HTMLElement {
  #x = 0;

  clicked() {
    this.#x++;
    window.requestAnimationFrame(this.render.bind(this));
  }

  constructor() {
    super();
    this.onclick = this.clicked.bind(this);
  }

  connectedCallback() { this.render(); }

  render() {
    this.textContent = this.#x.toString();
  }
}

window.customElements.define('num-counter', Counter);
复制代码

第一眼看到 #x 是否是以为很别扭,目前 TC39 委员会以及对此达成了一致意见,而且该提案已经进入了 Stage 3。那么为何使用 # 符号,而不是其余符号呢?

TC39 委员会解释道,他们也是作了深思熟虑最终选择了 # 符号,而没有使用 private 关键字。其中还讨论了把 private 和 # 符号一块儿使用的方案。而且还打算预留了一个 @ 关键字做为 protected 属性 。

来源于迷渡大大:为何 JavaScript 的私有属性使用 # 符号

zhuanlan.zhihu.com/p/47166400

在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式以下:

class Person {
  #name: string;

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

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

let semlinker = new Person("Semlinker");

semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
复制代码

与常规属性(甚至使用 private 修饰符声明的属性)不一样,私有字段要牢记如下规则:

  • 私有字段以 # 字符开头,有时咱们称之为私有名称;
  • 每一个私有字段名称都惟一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类以外访问,甚至不能被检测到。

说到这里使用 # 定义的私有字段与 private 修饰符定义字段有什么区别呢?如今咱们先来看一个 private 的示例:

class Person {
  constructor(private name: string){}
}

let person = new Person("Semlinker");
console.log(person.name);
复制代码

在上面代码中,咱们建立了一个 Person 类,该类中使用 private 修饰符定义了一个私有属性 name,接着使用该类建立一个 person 对象,而后经过 person.name 来访问 person 对象的私有属性,这时 TypeScript 编译器会提示如下异常:

Property 'name' is private and only accessible within class 'Person'.(2341)
复制代码

那如何解决这个异常呢?固然你可使用类型断言把 person 转为 any 类型:

console.log((person as any).name);
复制代码

经过这种方式虽然解决了 TypeScript 编译器的异常提示,可是在运行时咱们仍是能够访问到 Person 类内部的私有属性,为何会这样呢?咱们来看一下编译生成的 ES5 代码,也许你就知道答案了:

var Person = /** @class */ (function () {
    function Person(name) {
      this.name = name;
    }
    return Person;
}());

var person = new Person("Semlinker");
console.log(person.name);
复制代码

这时相信有些小伙伴会好奇,在 TypeScript 3.8 以上版本经过 # 号定义的私有字段编译后会生成什么代码:

class Person {
  #name: string;

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

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}
复制代码

以上代码目标设置为 ES2015,会编译生成如下代码:

"use strict";
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 _name;
class Person {
    constructor(name) {
      _name.set(this, void 0);
      __classPrivateFieldSet(this, _name, name);
    }
    greet() {
      console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
    }
}
_name = new WeakMap();
复制代码

经过观察上述代码,使用 # 号定义的 ECMAScript 私有字段,会经过 WeakMap 对象来存储,同时编译器会生成 __classPrivateFieldSet__classPrivateFieldGet 这两个方法用于设置值和获取值。介绍完单个类中私有字段的相关内容,下面咱们来看一下私有字段在继承状况下的表现。

5.2 ES 私有字段继承

为了对比常规字段和私有字段的区别,咱们先来看一下常规字段在继承中的表现:

class C {
  foo = 10;

  cHelper() {
    return this.foo;
  }
}

class D extends C {
  foo = 20;

  dHelper() {
    return this.foo;
  }
}

let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'
复制代码

很明显无论是调用子类中定义的 cHelper() 方法仍是父类中定义的 dHelper() 方法最终都是输出子类上的 foo 属性。接下来咱们来看一下私有字段在继承中的表现:

class C {
  #foo = 10;

  cHelper() {
    return this.#foo;
  }
}

class D extends C {
  #foo = 20;

  dHelper() {
    return this.#foo;
  }
}

let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'
复制代码

经过观察上述的结果,咱们能够知道在 cHelper() 方法和 dHelper() 方法中的 this.#foo 指向了每一个类中的不一样字段。关于 ECMAScript 私有字段的其余内容,咱们再也不展开,感兴趣的读者能够自行阅读相关资料。

6、总结

本文主要介绍了 JavaScript 中 WeakMap 的做用和应用场景,其实除了 WeakMap 以外,还有一个 WeakSet,只要将对象添加到 WeakMap 或 WeakSet 中,GC 在触发条件时就能够将其占用内存回收。

但实际上 JavaScript 的 WeakMap 并非真正意义上的弱引用:其实只要键仍然存活,它就强引用其内容。WeakMap 仅在键被垃圾回收以后,才弱引用它的内容。为了提供真正的弱引用,TC39 提出了 WeakRefs 提案。

WeakRef 是一个更高级的 API,它提供了真正的弱引用,并在对象的生命周期中插入了一个窗口。同时它也能够解决 WeakMap 仅支持 object 类型做为 Key 的问题。

7、参考资源

相关文章
相关标签/搜索