相信很多读者对 ES6 引入的 Map 已经不陌生了,其中的一部分读者可能也听说过 WeakMap。既生 Map 何生 WeakMap?带着这个问题,本文将围绕以下几个方面的内容为你详细介绍 WeakMap 的相关知识。
创建了一个 “重学TypeScript” 的微信群,想加群的小伙伴,加我微信 "semlinker",备注重学TS。目前已出 TS 专题 38 篇。
在计算机科学中,垃圾回收(Garbage Collection,缩写为 GC)是指一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。
垃圾回收最早起源于 LISP 语言,它有两个基本的原理:
JavaScript 具有自动垃圾回收机制,这种垃圾回收机制原理其实很简单:找出那些不再继续使用的变量,然后释放其所占用的内存,垃圾回收器会按照固定的时间间隔周期性地执行这一操作。
(图片来源:Garbage Collection: V8’s Orinoco)
局部变量只有在函数执行的过程中存在,在这个过程中,一般情况下会为局部变量在栈内存上分配空间,然后在函数中使用这些变量,直至函数执行结束。垃圾回收器必须追踪每个变量的使用情况,为那些不再使用的变量打上标记,用于将来能及时回收其占用的内存,用于标识无用变量的策略主要有引用计数法和标记清除法。
最早的也是最简单的垃圾回收实现方法,这种方法为占用物理空间的对象附加一个计数器,当有其他对象引用这个对象时计数器加一,反之引用解除时减一。这种算法会定期检查尚未被回收的对象的计数器,为零的话则回收其所占物理空间,因为此时的对象已经无法访问。
引用计数法实现比较简单,但它却无法回收循环引用的存储对象,比如:
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1引用o2
o2.p = o1; // o2引用o1
}
f();
为了解决这个问题,垃圾回收器引入了标记清除法。
标记清除法主要将 GC 的垃圾回收过程分为标记阶段和清除两个阶段:
JavaScript 中最常用的垃圾回收方式就是标记清除(mark-and-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。
相信很多读者对 ES6 中 Map 已经不陌生了,已经有了 Map,为什么还会有 WeakMap,它们之间有什么区别呢?Map 和 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; // 覆盖引用
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.js 和 weakmap.js 的输出结果,我们可知 weakmap.js 中定义的 arr 被清除后,其占用的堆内存被垃圾回收器成功回收了。
下面我们来大致分析一下出现上述区别的主要原因:
对于 map.js 来说,由于在 arr 和 Map 中都保留了数组的强引用,所以在 Map 中简单的清除 arr 变量内存并没有得到释放,因为 Map 还存在引用计数。而在 WeakMap 中,它的键是弱引用,不计入引用计数中,所以当 arr 被清除之后,数组会因为引用计数为 0 而被垃圾回收清除。
了解完上述内容之后,下面我们来正式介绍 WeakMap。
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。WeakMap 的 key 只能是 Object 类型。 原始数据类型是不能作为 key 的(比如 Symbol)。
new WeakMap([iterable])
iterable:是一个数组(二元数组)或者其他可迭代的且其元素是键值对的对象。每个键值对会被加到新的 WeakMap 里。null 会被当做 undefined。
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 的应用。
使用 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
在以下代码中,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。
在介绍 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 的私有属性使用 # 符号,https://zhuanlan.zhihu.com/
在 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 修饰符声明的属性)不同,私有字段要牢记以下规则:
说到这里使用 # 定义的私有字段与 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 这两个方法用于设置值和获取值。介绍完单个类中私有字段的相关内容,下面我们来看一下私有字段在继承情况下的表现。
为了对比常规字段和私有字段的区别,我们先来看一下常规字段在继承中的表现:
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 私有字段的其他内容,我们不再展开,感兴趣的读者可以自行阅读相关资料。
本文主要介绍了 JavaScript 中 WeakMap 的作用和应用场景,其实除了 WeakMap 之外,还有一个 WeakSet,只要将对象添加到 WeakMap 或 WeakSet 中,GC 在触发条件时就可以将其占用内存回收。
但实际上 JavaScript 的 WeakMap 并不是真正意义上的弱引用:其实只要键仍然存活,它就强引用其内容。WeakMap 仅在键被垃圾回收之后,才弱引用它的内容。为了提供真正的弱引用,TC39 提出了 WeakRefs 提案。
WeakRef 是一个更高级的 API,它提供了真正的弱引用,并在对象的生命周期中插入了一个窗口。同时它也可以解决 WeakMap 仅支持 object 类型作为 Key 的问题。
来自:https://segmentfault.com/a/1190000022756283
Javascript 一直是神奇的语言。 不相信我? 尝试使用map和parseInt将字符串数组转换为整数。打开 Chrome 的控制台(F12),粘贴以下内容,然后按回车,查看输出结果:
在 Javascript 中,一个函数可以传递任何多个数量的参数,即使调用时传递的数量与定义时的数量不一致。缺失的参数会以 undefined 作为实际值传递给函数体,然后多余的参数会直接被忽略掉
在JavaScript中,Map 是存储键/值对的对象。Map 类似于一般 JavaScript 对象 ,但对象与 Map 之间一些关键的差异使 Map 很有用。如果你要创建一个存储一些键/值路径的 JavaScript 对象
Map、Set的polyfill实现是可以继承的;//可继承的Array替换原生Array,Array要改的地比较多,除了替换原生Array还需修改继承函数,供参考
普通的 JavaScript 对象通常可以很好地保存结构化数据。但是它们有一些限制:只能用字符串或符号用作键,自己的对象属性可能会与从原型继承的属性键冲突(例如,toString,constructor 等)。对象不能用作键
Map的出现解决了传统object无法直接解决的问题,更好地向标准编程语言靠近(标准编程语言一般会提供Map集合),使用的坑也比较少(比如没有object作为key时转换为[object Object]的问题)。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
ES6的Map的键可以是任意的数据结构,并且不重复。那么map的底层原理是啥呢?Map利用链表,hash的思想来实现。首先,Map可以实现删除,而且删除的数据可以是中间的值。
JavaScript中,数组的遍历我们肯定都不陌生,最常见的两个便是forEach 和 map。(当然还有别的譬如for, for in, for of, reduce, filter, every, some, ...)
JS 普通对象 {key: value} 用于存放结构化数据。但有一件事我觉得很烦:对象键必须是字符串(或很少使用的 symbol)。如果将数字用作键会怎样? 在这种情况下不会有错误:
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!