javascript-Map/WeakMap/Set/WeakSet

loading 2022年10月21日 192次浏览

1. Map/Set

这两个就不在这写了,平时刷题也用得很多了,具体看看MDN上面的用法就好。
Map
Set

稍微记录一个小点:

1.1 Map和Object的区别和使用场景?

Map继承自Object,但是它们的区别有:

  1. 键的数据类型:
    1. 在 Object中,key必须是简单数据类型(整数/字符串/Symbol)
    2. 在 Map中则可以是JavaScript支持的所有数据类型
  2. 可迭代性
    1. Object自身不支持迭代,可以通过Object.entries()等方法辅助迭代
    2. Map可以用for...of/foreach等等来迭代,同理也只有Map可以使用"..."展开运算符
//判断一个对象能不能迭代可以通过
console.log(typeof obj[Symbol.iterator]); // undefined
console.log(typeof map[Symbol.iterator]); // function
  1. 获取size
    1. Object不能直接获取自身长度,只能通过Object.keys(obj).length这种方法计算
    2. Map自身有size属性,可以自己维持 size 的变化
  2. 有序性
    1. Object中的元素填入后不能按照原有顺序排序
      1. 首先遍历所有数值键,按照数值升序排列;其次遍历所有字符串键,按照插入时间排列;最后遍历所有Symbol键,按照插入时间排列
    2. Map中的元素会按输入顺序排好序
  3. JSON
    1. JSON支持Object,不支持Map

它们各自的使用场景如下:

Object:

  • 所要存储的是简单数据类型,并且key都为字符串或者整数或者Symbol的时候,优先使用Object,因为Object可以使用字符变量的方式创建,更加高效,不用慢慢set
  • 当需要在单独的逻辑中访问属性或者元素的时候,应该使用Object
var obj = {
    id: 1, 
    name: "It's Me!", 
    print: function(){ 
        return `Object Id: ${this.id}, with Name: ${this.name}`;
    }
}
console.log(obj.print());//Object Id: 1, with Name: It's Me.
// 以上操作不能用 Map 实现

Map:

  • Map是纯粹的hash, 而Object还存在一些其他内在逻辑,所以在执行 delete 的时候会有性能问题。所以插入删除密集的情况应该使用Map
  • Map在存储大量元素的时候性能表现更好,特别是在代码执行时不能确定key的类型的情况

2. WeakMap

WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

WeakMap中的weak描述的是JavaScript垃圾回收程序对待“弱映射”中键的方式。

强引用:a对象和b对象之间牵着一条链子,当a放开链子,b对象就会被垃圾回收。ab之间是强引用。
var obj = new Object();

弱引用:a对象和b对象之间牵着一条链子,c用手指着b对象,但是bc之间没有链子。那么当c不指着b了,b也不会被回收。bc之间是弱引用。
var obj = new WeakObject();

2.1 基本api

const key1 = {id: 1}, key2 = {id: 2}, key3 = {id: 3}; 

// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([ 
 [key1, "val1"], 
 [key2, "val2"], 
 [key3, "val3"] 
]); 

alert(wm1.get(key1)); // val1 
alert(wm1.get(key2)); // val2 
alert(wm1.get(key3)); // val3 

也可以通过set()创建:

const wm = new WeakMap().set(key1, "val1"); 
wm.set(key2, "val2").set(key3, "val3");

2.2 弱键

WeakMap持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。如下面这个例子:

const wm = new WeakMap(); 
wm.set({}, "val"); 

set()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标

来看另一个例子:

const wm = new WeakMap(); 
const container = { 
 key: {} 
}; 
wm.set(container.key, "val"); 
function removeReference() { 
 container.key = null; 
}

这一次,container对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉。

2.3 API

因为WeakMap中的键/值对任何时候都可能被销毁,所以没有提供迭代其键/值对的keys()/values()/entries()等方法,同样的,也就没有提供clear()这种一次性清除所有键值对的方法。

WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。

WeakMap只有以下方法:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

2.4 作用

Map的缺点:
给map设置值时会同时将键和值添加到键和值两个数组的末尾。从而使得键和值的索引在两个数组中相对应。当从该 map 取值的时候,需要遍历所有的键,然后使用索引从存储值的数组中检索出相应的值

这就导致了:

  • 赋值和搜索时间复杂度都是O(n),因为最坏情况需要遍历整个数组
  • 容易导致内存泄漏,因为数组会一直引用着每个键和值,使得垃圾回收算法不能处理它们。

当没有对象在引用该对象了,则该对象会被垃圾回收

而WeakMap:

  • 赋值和搜索时间复杂度都是O(1)?(存疑)
  • 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行

2.5 实例

2.5.1 DOM节点作为键名

下面代码中,document.getElementById('logo')是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。

let myWeakmap = new WeakMap();

myWeakmap.set(
  document.getElementById('logo'),
  {timesClicked: 0})
;

document.getElementById('logo').addEventListener('click', () => {
  let logoData = myWeakmap.get(document.getElementById('logo'));
  logoData.timesClicked++;
}, false);

2.5.2 用户计数器

现在有用于处理用户访问计数的代码,收集到的消息被存储在map中:用户名作为键,访问次数作为值。当一个用户离开时,应当将其对象垃圾回收。

        let visitsCountMap = new WeakMap(); // WeakMap: user => visits count

        // 递增用户来访次数
        function countUser(user) {
            let count = visitsCountMap.get(user) || 0;
            visitsCountMap.set(user, count + 1);
        }

        let john = { name: "John" };

        countUser(john);

        // 不久之后,john 离开了
        john = null;

3. WeakSet

WeakSet是Set的“兄弟”类型,其API也是Set的子集。WeakSet中的“weak”,描述的是JavaScript垃圾回收程序对待“弱集合”中值的方式。

3.1 基本api

同上面WeakMap类似,WeakSet的成员只能是对象

//a数组的成员,也就是数组,是对象,成为了WeakSet的成员
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}

//b数组的成员,也就是数字,不是对象,报错
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)

同样,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。

同理WeakMap,WeakSet不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。,WeakSet也没有遍历的api,只有:

  • WeakSet.add(val)
  • WeakSet.has(val)
  • WeakSet.delete(val)

这三个api

const ws = new WeakSet();
const obj = {};
const foo = {};

ws.add(window);
ws.add(obj);

ws.has(window); // true
ws.has(foo);    // false

ws.delete(window);
ws.has(window);    // false

3.2 作用和实例

WeakSet适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在WeakSet里面的引用就会自动消失。

如果用Set(),通过查询元素在不在disabledElements中,就可以知道它是不是被禁用了。不过,假如元素从 DOM 树中被删除了,它的引用却仍然保存在 Set 中,因此垃圾回收程序也不能回收它。
为了让垃圾回收程序回收元素的内存,可以在这里使用 WeakSet:

const disabledElements = new WeakSet(); 
const loginButton = document.querySelector('#login'); 

// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton);

下面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用WeakSet的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。

const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method () {
    if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
    }
  }
}

下面这个例子功能是追踪访问过网站的用户

        let visitedSet = new WeakSet();

        let john = { name: "John" };
        let pete = { name: "Pete" };
        let mary = { name: "Mary" };

        visitedSet.add(john); // John 访问了我们
        visitedSet.add(pete); // 然后是 Pete

        // 检查 John 是否来访过?
        alert(visitedSet.has(john)); // true

        // 检查 Mary 是否来访过?
        alert(visitedSet.has(mary)); // false

        john = null; //visitedSet将自动清除其中失效的值john