Set 和 Map 数据结构

Set

new Set()

Set 类似于数组,但是成员的值都是唯一的,没有重复的值。

属性:size

操作方法:add、delete、has、clear

遍历方法:keys、values、entries、forEach

Set 的应用场景

  1. 数组去重
const arr = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5];
console.log("通过扩展运算符将 Set 转为数组:", [...new Set(arr)]);
console.log("通过 Array.from 将 Set 转为数组:", Array.from(new Set(arr)));
  1. 字符串去重
console.log([...new Set("ababbc")].join(""));
  1. 实现并集、交集、差集
const a = new Set([1, 2, 3]);
const b = new Set([4, 3, 2]);

// 并集
const union = new Set([...a, ...b]);

// 交集
const intersect = new Set([...a].filter((x) => b.has(x)));

// 差集
const difference = new Set([...a].filter((x) => !b.has(x)));

WeakSet

new WeakSet()

WeakSet 与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:

  1. WeakSet 的成员只能是对象(除 null),而不能是其他类型的值
  2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用(故 WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息)
  3. WeakSet 不可遍历,即不具有 size 属性,和 clear 方法,以及 Set 的遍历方法

操作方法:add、delete、has

WeakSet 的应用场景

  1. 储存 DOM 节点,不用担心节点从文档移除时,引发内存泄漏

  2. 限制原型上的方法只能通过实例调用

const foos = new WeakSet();

class Foo {
  constructor() {
    foos.add(this);
  }

  method() {
    if (!foos.has(this)) {
      throw new TypeError("Foo.prototype.method 只能在 Foo 的实例上调用!");
    }
  }
}

Map

new Map()

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

属性:size

操作方法:set、get、has、delete、clear

遍历方法:keys、values、entries(默认迭代接口)、forEach

Map 与其他数据结构的互相转换

  1. Map 与数组
// 数组转 Map
const map = new Map([
  ["name", "张三"],
  ["title", "Author"]
]);

// Map 转数组
console.log([...map]);
  1. Map 与对象

如果 Map 有非字符串的键名,则这个键名会被转为字符串再作为对象的键名

const strMapToObj = (strMap) => {
  const obj = Object.create(null);
  for (let [k, v] of strMap) {
    obj[k] = v;
  }
  return obj;
};

对象转 Map

// 使用 Object.entries() 方法
const obj = { name: "张三", title: "Author" };
const map = new Map(Object.entries(obj));

// 自己转换
const objToStrMap = (obj) => {
  const strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
};
  1. Map 与 JSON

Map 转 JSON

// Map 的键名都是字符串时,可以选择转为对象 JSON
const strMapToJson = (strMap) => {
  return JSON.stringify(strMapToObj(strMap));
};

// Map 的键名有非字符串时,可以选择转为数组 JSON
const mapToArrayJson = (map) => {
  return JSON.stringify([...map]);
};

JSON 转 Map

// JSON 转为 Map,正常情况下,所有键名都是字符串
const jsonToStrMap = (jsonStr) => {
  return objToStrMap(JSON.parse(jsonStr));
};

// 有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组
const jsonToMap = (jsonStr) => {
  return new Map(JSON.parse(jsonStr));
};

WeakMap

new WeakMap()

WeakMap 与 Map 的区别有两点:

  1. WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
  2. WeakMap 的键名所指向的对象,不计入垃圾回收机制。WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用

操作方法:set、get、has、delete

WeakMap 的应用场景

  1. DOM 节点作为键名
let myWeakmap = new WeakMap();

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

document.getElementById("logo").addEventListener(
  "click",
  function () {
    let logoData = myWeakmap.get(document.getElementById("logo"));
    logoData.timesClicked++;
  },
  false
);
  1. 部署私有属性
// 使用了 WeakMap 来存储私有属性 _counter 和 _action
const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  /**
   * 构造函数
   * @param {number} counter - 计数器初始值
   * @param {function} action - 计数器为0时执行的回调函数
   */
  constructor(counter, action) {
    _counter.set(this, counter); // 使用 WeakMap 存储私有属性 _counter
    _action.set(this, action); // 使用 WeakMap 存储私有属性 _action
  }

  /**
   * 计数器减1
   */
  dec() {
    let counter = _counter.get(this); // 获取私有属性 _counter 的值
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter); // 更新私有属性 _counter 的值
    if (counter === 0) {
      _action.get(this)(); // 执行私有属性 _action 中存储的回调函数
    }
  }
}

WeakRef

WeakRef 对象,用于直接创建对象的弱引用

标准规定,一旦使用 WeakRef()创建了原始对象的弱引用,那么在本轮事件循环(event loop),原始对象肯定不会被清除,只会在后面的事件循环才会被清除

const target = { name: "张三" };
const ref = new WeakRef(target);

WeakRef 实例对象有一个 deref()方法,如果原始对象存在,该方法返回原始对象;如果原始对象已经被垃圾回收机制清除,该方法返回 undefined

WeakRef 的应用场景

  1. 缓存,未被清除时可以从缓存取值,一旦清除缓存就自动失效
const makeWeakCached(f) {
  const cache = new Map();
  return key => {
    const ref = cache.get(key);
    if (ref) {
      const cached = ref.deref();
      if (cached !== undefined) return cached;
    }

    const fresh = f(key);
    cache.set(key, new WeakRef(fresh));
    return fresh;
  }
}

FinalizationRegistry

清理器注册表功能 FinalizationRegistry,用来指定目标对象被垃圾回收机制清除以后,所要执行的回调函数

// 1. 新建注册表实例
const registry = new FinalizationRegistry((heldValue) => {
  // 回调函数的参数heldValue可以是任意类型的值,字符串、数值、布尔值、对象,甚至可以是undefined
  console.log("目标对象被清除了");
});

const target = {};
// 2. 注册表实例的register()方法,用来注册所要观察的目标对象
registry.register(target, "some value", target);

// 3. 如果还想取消已经注册的回调函数,则要向register()传入第三个参数,作为标记值。这个标记值必须是对象,一般都用原始对象
registry.unregister(target);

注册表不对目标对象构成强引用,属于弱引用。因为强引用的话,原始对象就不会被垃圾回收机制清除,这就失去使用注册表的意义了

FinalizationRegistry 的应用场景

  1. 对上述的缓存函数进行增强
function makeWeakCached(f) {
  const cache = new Map();
  const cleanup = new FinalizationRegistry((key) => {
    const ref = cache.get(key);
    if (ref && !ref.deref()) cache.delete(key);
  });

  return (key) => {
    const ref = cache.get(key);
    if (ref) {
      const cached = ref.deref();
      if (cached !== undefined) return cached;
    }

    const fresh = f(key);
    cache.set(key, new WeakRef(fresh));
    cleanup.register(fresh, key);
    return fresh;
  };
}
  1. 如果由于某种原因,Thingy 类的实例对象没有调用 release()方法,就被垃圾回收机制清除了,那么清理器就会调用回调函数#cleanup(),输出一条错误信息
class Thingy {
  #file;
  #cleanup = (file) => {
    console.error(
      `The \`release\` method was never called for the \`Thingy\` for the file "${file.name}"`
    );
  };
  #registry = new FinalizationRegistry(this.#cleanup);

  constructor(filename) {
    this.#file = File.open(filename);
    this.#registry.register(this, this.#file, this.#file);
  }

  release() {
    if (this.#file) {
      this.#registry.unregister(this.#file);
      File.close(this.#file);
      this.#file = null;
    }
  }
}

由于无法知道清理器何时会执行,所以最好避免使用它。另外,如果浏览器窗口关闭或者进程意外退出,清理器则不会运行

Last Updated: