如何理解WeakMap?

更新日期: 2021-06-11阅读: 1.6k标签: map

而且JavaScript既然已经有了Map类型的数据结构,为什么还有一种叫做WeakMap类型的数据结构呢?它和垃圾回收有什么关系?

WeakMap很早之前就遇到过,但是没有系统学习过,今天就来对它一探究竟。


初识WeakMap

WeakMap对象是一组键值对的集合,其中key是弱引用的
WeakMap的key必须是对象类型,value可以是任意类型

WeakMap的key为什么是弱引用的?

弱引用的意义:如果是作为key的对象没有任何地方引用它的话,垃圾收集器(GC)会将其标记为目标并且进行垃圾回收

WeakMap的key和value可以是哪些类型

key:必须是任意object类型(对象、数组、Map、WeakMap等等)
value:any(任意类型,所以也包括undefined,null)

WeakMap与Map最大的不同

WeakMap的key是不可枚举的,而Map是可枚举的。
不可枚举就意味着获取不到WeakMap的key列表。

设计为不可枚举的原因是因为:如果枚举WeakMap的key,那就需要依赖垃圾回收器(GC)的状态,从而引入不确定性。


新增WeakMap类型是为什么?

map api在js中可以通过共享4个API(get,set,has,delete)的两个数组来实现:一个存储key,一个存储value。在这个map上设置元素同步推入一个key和一个value到数组尾部。作为结果,key和value的索引会和两个数组绑定起来。从map获取一个值得话,会遍历所有key去找到一个匹配的,然后使用这个匹配到的index从values数组中查询到对应的值。

这样实现的话会有2个主要的弊端:

首先是set和search的时间复杂度是O(n),n是map中key数组的数量,因为都需要遍历列表去查找到需要的值
其次是会造成内存泄漏,因为数组需要无期限地去确保每个key和每个value的引用。这些引用会导致阻止key被垃圾回收掉,即使这个对象没有任何地方再引用到了,key对应的value也同样会被阻止垃圾回收。

相比之下,原生的WeakMap会保持对key的“弱”引用。原生的WeakMap不会阻止垃圾回收,最终会移除对key对象的引用。“弱”引用同样可以让value很好地垃圾回收。WeakMap特别适用于key映射的信息只有不被垃圾回收时才有价值的场景,换句话说就是WeakMap适用于动态垃圾回收key的场景。

因为引用是弱的,所以WeakMap的键是不能枚举的。没有方法去获取key的列表。如果枚举WeakMap的key,那就需要依赖垃圾回收器(GC)的状态,从而引入不确定性。如果必须要有key的话,应该去使用Map。


WeakMap的基本概念

语法

new WeakMap()
new WeakMap(iterable)

其中iterable是数组或者任意可以迭代的对象,需要拥有key-value对(一般是一个二维数组)。null会被当做undefined。

iterable为二维数组
const iterable = [
    [{foo:1}, 1], 
    [[1,2,3], 2], 
    [window, 3]
]
const iwm = new WeakMap(iterable)
// WeakMap {{…} => 1, Window => 3, Array(3) => 2}

实例方法

WeakMap.prototype.delete(key)

删除key关联的任意值。删除后WeakMap.prototype.has(key)返回false。

WeakMap.prototype.get(key)

返回与key关联的值,假设不存在关联值得话返回undefined。

WeakMap.prototype.has(key)

返回key在WeakMap上是否存在的结果。

WeakMap.prototype.set(key, value)

在WeakMap对象上为对应key设置指定的value。并且返回WeakMap对象


WeakMap最简使用方式

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); // WeakMap的值可以是任意类型,包括object和function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // key和value可以是任意对象。包括WeakMap!

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, wm2上没有o2这个key
wm2.get(o3); // undefined, 因为这是设置的值

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即使value是undefined)

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

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false


WeakMap存储私有数据

实例和原型链上的数据和方法是公开的,所以可以通过WeakMap类型的私有变量去隐藏实现细节。

const privates = new WeakMap();

function Public() {
  const me = {
    // Private data goes here
  };
  privates.set(this, me);
}

Public.prototype.method = function () {
  const me = privates.get(this);
  // Do stuff with private data in `me`...
};

module.exports = Public;


拥有clear方式的WeakMap类

class ClearableWeakMap {
  constructor(init) {
    this._wm = new WeakMap(init);
  }
  clear() {
    this._wm = new WeakMap();
  }
  delete(k) {
    return this._wm.delete(k);
  }
  get(k) {
    return this._wm.get(k);
  }
  has(k) {
    return this._wm.has(k);
  }
  set(k, v) {
    this._wm.set(k, v);
    return this;
  }
}
const key1 = {foo:1};
const key2 = [1,2,3];
const key3 = window;
const cwm = new ClearableWeakMap([
    [key1, 1], 
    [key2, 2], 
    [key3, 3]
])
cwm.has(key1) // true
console.log(cwm);// ClearableWeakMap {_wm: WeakMap {Window => 3, {…} => 1, Array(3) => 2}}
cwm.clear(); // 垃圾回收当前WeakMap,并且声称新的空WeakMap
cwm.has(key1) // false
console.log(cwm);// ClearableWeakMap {_wm: WeakMap {}}


WeakMap式自动垃圾回收缓存函数

实现缓存函数的方式有很多种,比如单次缓存,Map式全量缓存,LRU最近最少缓存等等。
那么为什么还需要WeakMap式的缓存函数呢?这是因为入参为对象类型的缓存且方便浏览器垃圾回收。

缓存函数实现

function memoizeWeakMap(fn) {
  const wm = new WeakMap();
  return function (arg) {
    if (wm.has(arg)) {
      return wm.get(arg);
    }
    const cachedArg = arg;
    const cachedResult = fn(arg);
    wm.set(cachedArg, cachedResult)
    console.log('weakmap object', wm)
    return cachedResult;
  };
}

let testFn = (bar) => {return Object.prototype.toString.call(bar)}; // 这里需要改造一下,改造完返回传入对象的类型

let memoizeWeakMapFn = memoizeWeakMap(testFn);

memoizeWeakMapFn(document) // weakmap对document生成缓存
memoizeWeakMapFn([1,2,3]) // weakmap对[1,2,3]生成缓存
memoizeWeakMapFn(function(){}) // weakmap对function(){}生成缓存

memoizeWeakMapFn(new WeakMap())  // weakmap对WeakMap实例生成缓存
memoizeWeakMapFn(new Map()) // weakmap对Map实例生成缓存
memoizeWeakMapFn(new Set())  // weakmap对Set实例生成缓存

WeakMap:
0: {Array(3) => "[object Array]"}
1: {function(){} => "[object Function]"}
2: {WeakMap => "[object WeakMap]"}
3: {Map(0) => "[object Map]"}
4: {#document => "[object htmlDocument]"}
5: {Set(0) => "[object Set]"}

如何体现出WeakMap的垃圾回收特性呢

// 忽略部分代码同上
setTimeout(()=>{
    memoizeWeakMapFn(document)    
},5000)

此时有时最后一次weakmap的打印结果如下:

WeakMap:
0: {#document => "[object HTMLDocument]"}
为什么说是“有时”?

因为打印时垃圾回收可能并没有执行完成,虽然会带来不确定性,但是可以确定的是,假设对象没有再被引用,WeakMap中的key会被浏览器自动垃圾回收掉。

为什么weakmap中仅保存了document?

这是因为[1,2,3], function(){},new WeakMap(),new Map(),new Set()在后面都没有再继续引用了,而且因为它们作为了WeakMap的key,所以会被浏览器自动垃圾回收掉。

如何不让key被垃圾回收掉呢?

保持一个变量对它的引用。

let memoizeWeakMapFn = memoizeWeakMap(testFn);
let retainArray = [1,2,3]; // 保持引用避免被垃圾回收
let retainMap = new Map(); // 保持引用避免被垃圾回收

memoizeWeakMapFn(document) // weakmap对document生成缓存
memoizeWeakMapFn(retainArray) // weakmap对[1,2,3]生成缓存
memoizeWeakMapFn(function(){}) // weakmap对function(){}生成缓存

memoizeWeakMapFn(new WeakMap())  // weakmap对WeakMap实例生成缓存
memoizeWeakMapFn(retainMap) // weakmap对Map实例生成缓存
memoizeWeakMapFn(new Set())  // weakmap对Set实例生成缓存

setTimeout(()=>{
    memoizeWeakMapFn(document)    
},5000)

此时打印结果为:

WeakMap:
0: {#document => "[object HTMLDocument]"}
1: {Map(0) => "[object Map]"}
2: {Array(3) => "[object Array]"}

这是因为[1,2,3], new Map()被变量retainArray和retainMap持续引用着,所以不会被垃圾回收。而function(){},new WeakMap(),new Set()都没有再继续引用了,而且因为它们作为了WeakMap的key,所以会被浏览器自动垃圾回收掉。

如果手动触发垃圾回收呢?

可以借助Chrome DevTools的memory面板工具,有一个手动触发垃圾回收的按钮。

// ...
setTimeout(()=>{
    memoizeWeakMapFn(document)    
},5000)

比如在上面的例子中,设置了一个5秒的延时:只要代码运行后的5秒内,去手动触发“垃圾回收按钮”,就可以很精确地看到WeakMap的key被垃圾回收了。

当然5秒这个时间是可以人为调整的,保证自己能在setTimeout内的代码运行前触发对WeakMap的垃圾回收即可,可以适当调大。

来自:https://segmentfault.com/a/1190000040163271

链接: https://fly63.com/article/detial/10418

JS 中为啥 [‘1‘, ‘7’,‘11’ ].map(parseInt) 返回 [1, NaN, 3]

Javascript 一直是神奇的语言。 不相信我? 尝试使用map和parseInt将字符串数组转换为整数。打开 Chrome 的控制台(F12),粘贴以下内容,然后按回车,查看输出结果:

为什么 [‘1’, ‘7’, ‘11’].map(parseInt) 的结果是 [1, NaN, 3]?

在 Javascript 中,一个函数可以传递任何多个数量的参数,即使调用时传递的数量与定义时的数量不一致。缺失的参数会以 undefined 作为实际值传递给函数体,然后多余的参数会直接被忽略掉

js中Map

在JavaScript中,Map 是存储键/值对的对象。Map 类似于一般 JavaScript 对象 ,但对象与 Map 之间一些关键的差异使 Map 很有用。如果你要创建一个存储一些键/值路径的 JavaScript 对象

typescript中继承Array、Map、Set报错的解决

Map、Set的polyfill实现是可以继承的;//可继承的Array替换原生Array,Array要改的地比较多,除了替换原生Array还需修改继承函数,供参考

用map代替纯JavaScript对象

普通的 JavaScript 对象通常可以很好地保存结构化数据。但是它们有一些限制:只能用字符串或符号用作键,自己的对象属性可能会与从原型继承的属性键冲突(例如,toString,constructor 等)。对象不能用作键

ES6的Map类型

Map的出现解决了传统object无法直接解决的问题,更好地向标准编程语言靠近(标准编程语言一般会提供Map集合),使用的坑也比较少(比如没有object作为key时转换为[object Object]的问题)。

es6 Set和Map数据结构

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

ES6 Map 原理

ES6的Map的键可以是任意的数据结构,并且不重复。那么map的底层原理是啥呢?Map利用链表,hash的思想来实现。首先,Map可以实现删除,而且删除的数据可以是中间的值。

js中forEach & map

JavaScript中,数组的遍历我们肯定都不陌生,最常见的两个便是forEach 和 map。(当然还有别的譬如for, for in, for of, reduce, filter, every, some, ...)

何时使用 Map 来代替普通的 JS 对象

JS 普通对象 {key: value} 用于存放结构化数据。但有一件事我觉得很烦:对象键必须是字符串(或很少使用的 symbol)。如果将数字用作键会怎样? 在这种情况下不会有错误:

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!