信任很多读者对 ES6 引入的 Map 现已不陌生了,其间的一部分读者可能也听说过 WeakMap。既生 Map 何生 WeakMap?带着这个问题,本文将环绕以下几个方面的内容为你详细介绍 Weakw y 6 * G n $Map 的相关常识。

你不知道的 WeakMap

O 9 : u O 9、什么是废物收回

在计算机科学中,废物收回(Garbage Collection,缩写为 GC)是指一种主动的存储器管理机制。当2 f x m 8某个程序占用的一部分内存空间不再被这个程序拜访时,这个程序会借助废物收回算法向操t Q F 8 } ( t =作系统偿还这部分内存空间。废物收回器能够减z C . 1 @轻程序员的担负,也减少o P G p程序中的错误。

废物收回最早起源于 LISP 言语,它有两个基本的原F : H u ! 7理:

  • 考虑某个方针在未来的程序运行% 6 E中,将不会被拜访;
  • 收回这些方针所占用的存储器。

JavaScript 具有主动废物收M v 4 Y 6回机制,这种废物收回机制原+ a j Y n理其实很简略:找出那些不再持续运用的变量,然后开释其所占用的内存,废物F / W 4 u O c G收回器会按照固定的时间间隔周期性D [ ` x [ 1 # c .地履行这一操作。

你不知道的 WeakMap

(图片来历:Garbage Collectionu – (: V8’s Oz ] g V ^ k . 2rinoco)

局部变量只需在函数履行的进程中存在| ~ . p 0 1 p,在这个进程中,一般v g 5 , X C ] N 0情况下会为局部变量在h V *栈内存上分配空间,然后在函数中运用这些变量,直至函数履行结束。废物收回器有必要追寻每个变量的运用情况,为那些不再运用的变X h C ~ K { Z z量打上符号,用于将来能及时收回其占用的内存,用于标识无用变量的战略首要有引证计数法和符号铲除法。

1.1 引证计数法

最早的也是最简略的废物收回完成办法,这种办法为占用物理空间的方针附加一个计数器,当1 F V v有其他方针引证这个方针时计数器加一,反之引证免除时减一。这种算法会定时查看尚未被收回的方针的计G $ | h r数器,为零的话则收回其所占物理空间,由于此刻的方针现已无法拜访。

引证计数法完成比较简略,但它却无法收回循环引证的存储方针,比方:

function f() {
var o1 = {};
var o2 = {};K I 9
o1.p = o2; // o1引证o2
o2.p = o1; // o2I Q - M @ 3引证o1
}
f();

为了处理这个问题,废物收回器引入了符号铲除法。

1.2 符号铲除法H 3 S ~

符号铲除法首要将 GC 的废物收回进程分& } B u [ h ~ , ^为符号阶段和铲除两个阶段:

  • 符号阶段:把一切活动方针做上符号;
  • 铲除阶z p 0 ! 6 + P b段:把没有符号(也便是非活动方针)销毁。

JavaScript 中最常用的废物收回方式便是符号铲除(mark-and-sweep),当变量进入环境时,就将这个变量符号 2 . D m c s M “进入环境”,当变量脱离环境时,就将其符号为 “脱离环境”。

符号铲除法详细的废0 _ ? N 6 Q ,物收回进程如下图所示:

你不知道的 WeakMap

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

在日常工作中,关于不再运用的方针,一般咱们会期望它们会被废物收回器收回。这时,你能够运用 nul] T ! } ! 4l 来掩盖对应方针的引证,比方:

let sem = { name: "Semlinker" };
// 该方针能被拜访,sem是它的引证
s[  * * S [em = null; // 掩盖引证
// 该方针G F O {将会被从内存中铲除

但是,当方针、数组这类数据结构在内存中时,它们的子元素,如方针的特点、数组的元素都是能够拜访的。例如,假如把[ +一个方针放入到数组中,那么只需这个数组存在,那么这个方针也就存在,即便没有其他对该方针的引证。比方:

let sem = { name: "Semlinker" };
let array = [ sem ]7 S C 9;
sem = null; // 掩盖引证
// sem 被存储在数组里, 所以它不会被废物收回机制收回j i & X 8
// 咱们能够经过 array[0] 来获取它

相同,假如咱们运用方针作为惯例 Map 的键,那么当 Map 存在时,该方针也将存在。它会占用内存,并且不会被废物收回机制收回。比方:

let sem = { name: "Semlinker" };
let map = new Map();
map.set(sem, "全栈修仙之路");
sem = nullK y u 2 X 2; // 掩盖引证
// sem被存储在map中
// 咱们能够运用map.keys()来获取它

那么如何处理上述 Map 的废物收回问题呢?这时咱们就需求来了解一下 WeakMap。

二、为什么需求 WeakMap

2.1 Map 和 WeakMapY m y : H ] Q vk B Z ~ j J + ] =差异

信任很多读者对 ES6 中 Map 现已不陌生了,现已有了 Map,为什么还会有 WeakMap,它们之间有什么差异呢?Map 和 WeakMap 之间的首要差异:

  • Map 方针的键能够是任何类型,但 WeakMap 方针中的– ! * g d键只能是方针引证;
  • WeakM= A S H ) o N Tap 不能包括无引证的方针,否则2 R t d会被主动铲除出调集(废物收回机制);
  • WeakMap 方针是不可枚举的,无法获取调集的巨细。

在 JavaScript 里,Ma1 t n : o m W F :p API 能够经过使其A I h 0 F b c – 四个 API 办法共用两个数组(一个寄存键,一个寄存值)来完成。J a # 9 B r给这种 Map 设置值时会一起将键和值增加到这两个数组的末尾。然后使得键和值的索引在两个数组中相对应。当从该 Map 取值的时候,需求遍历一切的键,然后运用索引从存储值的数组中检索出相应的值。

但这样的完成会有两个很大的缺陷,首要赋值和查找操作都是 O(n) 的时间复杂度(n 是键值对的个数),由于这两个操作都需求遍历悉数整个数组来进& ] 6 a ] = S V行匹配。别的一个缺陷是可能会导致内存走漏,由于数组会一向引证着每个键和值。; c , j L j { O 9 这种引证使得废物收回算o 5 S 0 k { s S h法不能收回处理他们,即便没有其他任何引证存在了。

相比之下j $ c ( ; _ ] n a,原生的 WeakMap 持有的是每个键方针的 “弱引证7 9 s 9”,这意味着在没有其他引证存在时废物收回能正确进行。 原生 WeakMap 的结构是特别且有用的,其用于映射` M ? O l o的 key 只需在其没有被收回时才是有用的。

正由于这样的弱引证,WeakMap 的 key 是不可枚举的 (没有办法能给出一切的 key)。假如key 是可枚举的话,其列表将会受废物收回机制的q ( d影响,然后得到不确定的成果。因此,假如你想要这种类型方针的 key 值的列表,你应该运用 Mf % B :ap。而假如你要往方针上增h M } m f V B加数据,又不想干扰废物收回机制,就能够运用 We0 $ `akMap。6 i X

所以关于前面遇到的废物收回问题,咱们能够运用 WeakMap 来处理,详细如下:

let sem = { name: "Semlinker" };
let map = new WeakMap();
map.set(sem, C N T ! s ,"全栈修仙之路");
sem = null; // 掩盖引证

2.2 Weak, 5 + & / k _ }Map 与废物收回

Weaku 0 . ? d z A CMap 真有介绍的那么奇特么?下面咱们来着手测验一下同个场景下 Map 与 Wea0 ^ g # p J FkMap 对废物收回的影响。首要咱们c e = 6分别创立两个文件:map.js 和 weakmap.jsA ; ` 0 x W ^

map.js

/H ) Z . R/map.js
function usageSize() {
const used = process.memor9 w MyUsage().heapUsed;
return Math.round((used / 102x c 4 , $4 / 1024) * 100) / 100 + "M";
}
global.gc();x P j  9 N
console.log(usJ ~ Q  VageSize()); // ≈ 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();
consou r d *le.log(usageSize()); // ≈ 83.2M

创立完 map.js 之后,在命令行输入 node --expose-gc map.js 命令履行 map.js 中的代码,其间 --expose-gc 参数表明允许手动履行废物收回机制。

weakmap.js

function usageSizK 0 x B O +e() {
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(usageSm 5 j D 0 W [ Fize()); // ≈ 3.2M

相同,创立完 weakmap.js 之后,在命令行输入 node --expose-gc weakmap.js 命令履行 weakmap.js 中的代码。经过对比 map.jsweakmap.js 的输出成果U F ` $ ~,咱们可知 weakmap.js 中界说的 arr 被铲除后,其占用的堆内存被废物收回器成功收回了。

下面咱们来大致剖j u 3析一下呈现上述差异的首要原因:

关于 map.js 来说,由于在 arr 和 Map 中都保留了数组的强引证,所以在 Map 中简略的铲除 arr 变量内存并没有得到开释,由于 Map 还存在引证计数。而在 WeakMap 中,$ r & e G它的键是弱引证,不计入引证计数中,所以当 arr 被铲除之后,数组会由于引证计数为 0 而被废物收回铲除。

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

三、WeakMap 简介

WeakMap 方针是一组键/值对的调集,其间的键是% [ a d 0 V S J O弱引证的。WeakMap 的 key 只能是 Object 类型。 原始数据类型是不能作为 key 的(比方 Symbol)。

3.1 语法

new WeakMaq : V + a wp([iteq I drable])

iterable:是一个数组(二元数组)或许其他可迭代的且其元素是键值对的方针。每个键值对会被加到新的 WeakMap 里。null 会被作为 undefined。

3.2 特点

  • length:特点的值为 0;
  • prototH d l i = - O 8ypeWeakMap 结构器的原型。 允许增加特点到一切的 Weak4 c &Map 方针。

3.3 办R V u w p , /

  • WeakMap.prototype.delete(key):移除 key 的相关* l e ~ W Y c #方针。履行后 WeakMap.prototype.has(key) 回来false。
  • WeakMap.prototype.get(key):回来 key 相关方针,或许 undefined(没有 key 相关方针时)。
  • WeakMap.prototype.has(key):根据是否有 key 相关方针回来一个布尔值。
  • WeakMap.proS + H _ L 7 w (totype.1 , kset(key, value):在 WeakMap 中设置一组 keS B B + 5y 相关方针,回来这个 WeakMap 方针。

3.4 示例

const wm1 = new WeakMap(),
wm2 = new WeakMap(),
wm3 = new WeakMap(4 V 9 E A =);
const o1 = {},
o2 = function(){},
o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azer1 c e + m * y S Qty+ . q . _");
wm2.set(o1, o2); // value能| l & X d & } Q ]够是恣意值,包括一个方针或一个函数
wm* k 6 ,2.set(o3, undefined);
wm2.set(wm1, wm2); // 键和值能够是恣意方针,乃至别的一个WeakMap方针
wm1.get(o2); // "azerty"
wm2.get(o2e m Y); // 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` E h); // 37
wm1.has(o1);   // true
wm1.dj A N 9 G A w (elete(o1);
wm1.has(o1);   // false

介绍完 Weak0 + CMap 相关的基础常识,下面咱们来介绍一下 WeakMap 的运用。

四、WeakMap 运用

4.1C $ 4 O Z p 经过 WeakMap 缓存计算成果

运用 WeakMap,你能够将先前计算的成– r k * $ L @ + M果与方针相相关,而不必担心内存管理。以下功能 countOwnKeys() 是一个示例:它将曾经的成果缓存在 WeakMap 中 cache

coO F jnst cache = new WeakMaN = t j r x rp();
function countOwnKeys(obj) {
if (cache.has(obj)) {
re9 s G a r r @ bturn [cache.get(obj),& J p &  + T 'cached'];
} else {
const count = Obje4 m o 6 _ / 7 5ct.keys(obj).length;
cache.set(obj, count);
reo F Aturn [count, 'compB _ q W | j Tuted1 2 Z u 8 '];
}
}

创立完 countOwnKey8 , `s 办法,咱们来详细测验一下:

le3  jt obj = { naH 7 x 3 X eme: "kakuqo", age: 30 };
console.log(countOwnKeys(obj)i Y [);
// [E ] J = k2, 6 F ; & . P # ] z'computed']= a & | V c
console.log(countOwnKeys(obj));
// [2, 'cached']
obj = null; // 当方针不在运用时,设置为null

4.2 在 Weax _ FkMap 中保留私有数据

在以下代码中,WeakMap _counter_action 用于存储以下实例的虚拟特点的值:

conu  O T v 6 Z Lst _counter = net 1 9 H v - m L Aw WT ^ 0 D u KeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, actu R P B T Oion) {
_counter.set(this, counter);
_action.set(this, a` R { q Lction);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(thin O _ E [ ;s, counter);
if (counter === 0) {
_action.get(this)();
}
}
}

创立完 Countdown 类,咱们来详细测验一下:

let invoked = false;
const countDown = new Countdown(3, () => invo: 8 7 T Y J nked = t5 C - m J N m n aru[ ) g le);
countDown.dec();
countDown.dec();
countDown.dec();
console.log(`invoke= y A ]d status: ${invoked}`)

说到类的私@ # v有特点,咱们, d 4 & m U a V +不得提一下 ECMAScript Private Fields。

五、ECMAScript 私有字段

5.1 ES 私有字段简介

在介绍 ECMAScri5 * @ K J v B ~ .pt 私有字段前R } 3 x K y j,咱们先目击一下它的 “芳容”:

class Counter extA a . O k . %ends HTMLElement {
#x = 0;
clicked() {
this.#x++;
window.requestAnimatiot D T K X U #nFrame(this.render.bind(this));
}
cow / A B K # : ^nstructor() {
super();
this.onclick = this.clicked.bind(this);
}
connec~ m ^ R | ` tedCallback() { this.render(); }
render() {
this.textContent = this.#x.toStringL _ K l ? F 2 4 ^();f D s f ] 8 T ?
}
}
window.customElements.define('num-counter', Counter);

第一眼看到 #x 是不是觉得很别扭,现在 TC39 委员会以及对此达成了M & M q C @ o :一致意见,并且该提案现已进入了 Stage 3。那么为什么运用 # 符号,而不是其他符号呢?

TC39 委员会解释道,他们也是做了深思熟虑最终挑选了 # 符号,而没有运用 private 关键字。其间还讨论了把 pQ C q frivate 和 # 符号一起运用的计划。并且还打算预留了一个 @ 关键字作为 protected 特点 。v M {

来历于迷渡大大:为什么 JavaScriptL R A C G M 6 的私有特点运用 # 符号

zhQ , t =uanlan.zhihu.com/p/47166400

TypeScript 3.8 版本就开端支Q h z f . $ RECMAScript 私有字段,运用方式如下:

class Person {
#Z  F = 7 5 A Q 0name: string;
consG g # 3 1 D K ( gtructor(name: strinS 4 R a - q . i Zg) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}E { ` / L e F _ W
}
let semlinker = new PersonQ $ q ~ l 9("Semlinker");
semlinker.#name;
//     ~~~~~
// Property '#naX G 1 0 | Ume' is not accessible outside class 'Person'
// becausV 9 U h he it has a private identifier.

与惯例特点(乃至运用 private 修饰符声明X o ? z V } (的特点)不同,私有字段要) ) w c – : [ q M紧记以下规则:

  • 私有字段以 # 字符开头,有时咱们称之为私有称号;
  • 每个私有字段称号都仅有地限定于其包= E L H – 3 C . o括的类;
  • 不能在私有字段上运用 TypeScript 可拜访性修饰符(如 public 或 private);
  • u T { F q C K 5有字段不能在包括的类之外拜访,乃至不能被检测到。

说到V 7 Q 2 0这里运用 # 界说的私有字段与 private 修饰符界说字段有什么差异呢?现在咱们先来看一个 privat1 c / { 1 K l QeB o & 8 I k 的示例:

class Person {
constructor(private nV d M ? _ k 7 eame: string){}
}
let pe= B H v M 6  7 prson = new Person("Semlinker");
console.log(person.name);

在上面代码中,咱们创立了一个 Person 类,该类中运k a $private 修饰符界说了一个私有特点 name,接着运用该类创立一个 person 方针,然后经过 person.name 来拜访 person 方针的私有特点,这时 TypeScript 编译器会提示以下异常:

Property 'name' is private and only accessible within class 'PerD m 4son'.(2341)

那如何处理这个异常呢?当然你能Z k N q ! a S够运用类型断言把 person 转为 any 类型:

console.log(, e z A(person as any).name);

经过这种方式虽5 f t K j X c r然处理了 TypeScript 编译器的异常提示,但是在运行时咱们仍是能够拜访到 Person 类内部的私有特点,为什么会这样呢?咱们来看一下编译生成的$ w U ES5 代码,或许你就知道答案了:

var Person = /** @class */ (function () {
function Per V w G J 8 Sson(name) {
this.l X U = [ Sname = name;
}
retu. C 7 %  = 3 rn Person;
}());
var person = new Person(o [ q * | 5 c K ,"Semlin( D = R Fker");
console.log(person.name);

这时信任有些小伙伴. ! j H d T会好奇,在 TypeScript 3.8 以上版本经过 # 号界说的私有字段编译后会生成什么代码:

class Person {
#name: string;
cond v y l Hstructor(name: string) {
tha U }is.#name = name;a R c w ^
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}

以上代码方针设置为 ES201y N k Y 75,会编译生成以下代码:

"use strict";
var __classP8 _ 6 ,rivateFieldSet = (this && this.__classPrivateFieldSet)
|| function (receiver, privateMap, value) {
if (!privateMap- } ) h.has(receiver)) {
throw new TypeErroV d = hr(7 0 j"attempted to set private field on non-instance");
}
privateMap.set(receiver, value);
retur[ ` 5 . / w W Vn value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)
|| function (receiver, privateMap) {
if (!- g P n K U 8 1privateMap.has(receiver))_ ^ d K c ! y {
throw new TypeError("attempted to get private field on non-instance");. ] A -  h
}
return pr* 9 D W D g 0ivateMap.get(receiver);
};
var _name[ ! q N ! / b i;
class Person {
constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, namh Y Ze);
}
greet() {
console.log(`Hello, my name is ${__classPrivateField} m ! mGet(this, _name)}!. T G - ? @ q = 6`);
}
}
_name = new WeakMap();

经过调查上述代码,运用 # 号界说的 ECMAScript 私有字段,会经过 WeakMap 方针来存储,一起编译器会生成 __classPrivateFieldSet__clas? L nsPrivateFieldGet 这两个办法用于设置值和B ) _ } ( } 7获取值。介绍完单个类[ F p B @ * @ 中私有字段的相关内容,下面咱们来看一下私有字段在承继情况下的体现。

5.2 ES 私有字段承继

为了对B { j O l 3 U Y {比惯$ K V % 6例字段和私有字段的差异,咱们先来看一下惯例字段在承继中的体现:

class C {
foo = 10;
cHelper() {
return thi= I } 5 v Q os+ k D.foo;
}
}
class D extends C {
fom L bo = 20;
dHelper() {
return this.foo;
}
}
let instance = new D();
// 'this.foo'F I e 8 Z / ` refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'

很明显不管是调用子类中界说的 cHelper() 办法仍是父m 1 L g v类中界说的 dHelper() 办法最终都是输出子类上的 foo 特点。接下来咱! P E们来看一下私有字n @ 6 u !段在承继中的体现:

class C {
#foo = P A 10;
cHelper() {
return this.#foo;
}
}
class D extenQ Q k d _ 9 P ~ ds C {
#foo = 20;
dHelper() {
return this.#foo8 B 9 t d O m w;
}
}
let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
conso`  &le.log(instance.dHelper()); // prints '20'

经过调查上述的成果,咱们能够知道在 cHelper() 办法和 dHel^ P 2 cper()e c 9 - n D * P 办法中的 this.#foo 指向了每个类中的不同字段。关于 ECMASx ~ ` q 0cript 私有字段的其他内容,咱们不再打开,感兴趣的读者能够自行i w s阅览相关材料。

六、总结

本文首要介绍了 JavaScri$ 4 . Spt 中 WeakMap 的效果和运用场景,其实除^ M T Z B / E了 WeakMap 之外,还有一个 WeakSet,只需将方针增加到 WeakMap 或 WeakSet 中,GC 在触发条件时就= ( q P p }能够将其占用内存收回。

但实际上 JavaScript 的 WeakMap 并不是真实z j x V 5 8 N T意义上的弱引证:其实只需键依然存活,它就强引证其内容。Weak` ; @ z JMap 仅在键被废物收回之后,才弱引证它的内容。为了供给真实的弱引证,TC39 提出了 WeakRefs 提案。

Wea6 } 0 ` C Z z | kRef 是一个更高档的 API,它供给了& { a H ] 8实的弱引证,并在方针的生命周期中插入了一个窗口。一起它也能够处理 WeakMap 仅支撑 object 类型作为 Key 的问题。

七、参考资源

  • MDN – WeakMap
  • exploringjs – ch_weakma( X Z d ! M Pps
  • typescriptlang – ecmascript-private-fields
  • what-are-the-actual-uses-of-es6-weakmap
  • JavaScript废物收回
  • What’s New in JavaScript
  • 简略了解 JavaScript 废物收回机制
  • java: U z m ^ ` !script.info – weakmap-weakset
  • 为什么 JavaS} ` ( L /cript 的私有特点运用 # 符号

创立了一个 “重学TypeSS 0 ! Q s B {cripL z t * @ t At” 的微信群,想加群的小伙伴,加我微信 “semlinker”,补白重学TS。现在已有 TJ F a % (S 系列文章 38 篇。

你不知道的 WeakMap