如何理解 Set、Map、WeakSet、WeakMap

远子 •  2021年07月22日

最近发现我们公司的代码里几乎没有使用 Set 和 Map,我自己也很少使用,大多数场景下都在使用 Array 和 Object。

Set 和 Map 是 ES6 提供的新的数据结构,Set 可以理解自带去重功能的 ArrayMap 可以理解为加强版的 Object,理解 Set 和 Map 很有必要。

Set 和 Array

Set 是一种叫做集合的数据结构,Set 和 Array 很像:

const mySet = new Set([1, 2, 3, 4]);
console.log(mySet); // 输出:Set(4) {1, 2, 3, 4}

const myArray = [1, 2, 3, 4];
console.log(myArray); // 输出:[1, 2, 3, 4]

Set 中的元素是唯一的,而 Array 中的元素可以重复:

const mySet = new Set([1, 1, 2, 2, 3, 4]);
console.log(mySet); // 输出:Set(4) {1, 2, 3, 4}

const myArray = new Array([1, 1, 2, 2, 3, 4]);
console.log(myArray); // 输出:[1, 1, 2, 2, 3, 4]

操作 Set 和 Array 的方法也大同小异:

SetArray
头部添加元素 myArray.unshift(1)
尾部添加元素mySet.add(1)myArray.push(1)
移除头部元素 myArray.shift()
移除尾部元素 myArray.pop()
清空元素mySet.clear()myArray = []
删除特定元素mySet.delete(1)根据下标删除
判断是否包含某元素mySet.has(1)myArray.includes(1)
遍历mySet.forEach()myArray.forEach()
获取元素个数mySet.sizemyArray.length

在添加元素的时候,Set 可以链式调用,比如:mySet.add(1).add(2).add(3),这是因为 mySet.add 方法返回的是 Set 对象本身。

myArray.push 方法返回的是数组新的长度,因此不能链式调用。

可以通过 Object.prototype.toString.call(mySet) === '[object Set]' 判断一个变量是否是 Set 类型,事实上 FunctionDateRegExp 都可以这样判断,来看一下 underscore 里 isSet 方法的定义:

// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError, isMap, isWeakMap, isSet, isWeakSet.
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) {
  _['is' + name] = function(obj) {
    return toString.call(obj) === '[object ' + name + ']';
  };
});

可以通过 Array.isArray(myArray) 判断一个变量是否是 Array 类型。

写到这里我有两个疑问:

  1. Array.isArray() 明显更符合语义,为什么不扩展 Set.isSet()Function.isFuntion() 等等方法呢?
  2. Set 为什么只有 add 方法,为什么不提供类似 Array 的 pop、shift、unshift 方法呢?

Map 和 Object

Map 是一种叫做字典的数据结构,Map 和 Object 很像:

const myMap = new Map();
myMap.set("name", "rmlzy");
myMap.set("email", "rmlzy@outlook.com");
console.log(myMap); // 输出:Map(2) {"name" => "rmlzy", "email" => "rmlzy@outlook.com"}

const myObject = new Object();
myObject.name = "rmlzy";
myObject.email = "rmlzy@outlook.com";
console.log(myObject); // 输出:{name: "rmlzy", email: "rmlzy@outlook.com"}

Map 和 Object 存储的都是键值对,我们通常把对象当做 Map 来使用,但两者有一些重要的差别:

  • 第一个区别:Map 的键可以是任意值,而 Object 的键只能是 String 或 Symbol;
  • 第二个区别:Object 有原型链,原型链上的键名有可能和你自己在 Object 上设置的键名冲突,而 Map 可以包含任意的键;
  • 第三个区别:Map 中的键是有序的,当遍历 Map 的时候,会按照插入顺序返回键值,而 Object 的键是无序的;
  • 第四个区别:Map 可以直接迭代,Object 需要先获取它的键然后才能迭代;
  • 第五个区别:在频繁增删键值对的场景下 Map 的性能更好。

另外,Map 是这样判断键是否相等的:

  • NaNNaN 相等;
  • 在目前的ECMAScript规范中,目前 -0+0 是相等的;
  • 剩下的所有值根据 === 运算符的结果判断是否相等。

Set 和 WeakSet

WeakSet 是一种特殊的 Set,特殊之处是 Set 可以存储任意类型,而 WeakSet 只能存储对象。

WeakSet 的出现主要解决弱引用对象存储的场景,WeakSet 集合中的对象为弱引用,如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。

这些意味着 WeakSet 中没有存储当前对象的列表,这也造成 WeakSet 是不可枚举的。

Map 和 WeakMap

WeakMap 是一种特殊的 Map,和 WeakSet 类似,Map 的键可以是任意类型,而 WeakMap 的键只能是对象类型。

WeakMap 键名所指向的对象,不计入垃圾回收机制。

同 WeakSet 一样,因为是弱引用,WeakMap 也是不可枚举的。

(完)