Map、WeakMap、Set、WeakSet

66 阅读13分钟

Map

基本API

使用 new 关键字和 Map 构造函数可以创建一个空映射: const m = new Map();

如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含 键/值对 数组。
可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中:

// 使用嵌套数组初始化映射
const m1 = new Map([
  ['key1','valu1'],
  ['key2','valu2'],
  ['key3','valu3'],
]);
console.log(m1); // Map(3) { 'key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3' }


// 使用自定义迭代器初始化映射
const m2 = new Map({
  [Symbol.iterator]: function*() {
    yield ["key1", "val1"];
    yield ["key2", "val2"];
    yield ["key3", "val3"];
  }
});
console.log(m2); // Map(3) { 'key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3' }

// 映射期待的键/值对,无论是否提供
const m3 = new Map([[]]); 
console.log(m3.has(undefined)); // true 
console.log(m3.get(undefined)); // undefined

初始化之后,可以使用 set()方法再添加键/值对。另外,可以使用 get()has()进行查询,可 以通过 size 属性获取映射中的键/值对的数量,还可以使用 delete()clear()删除值。

const m = new Map();
console.log(m.has("firstName"));  // false
console.log(m.get("firstName"));  // undefined
console.log(m.size);              // 0

m.set("firstName", "Matt")
 .set("lastName", "Frisbie");
console.log(m.has("firstName")); // true
console.log(m.get("firstName")); // Matt
console.log(m.size);             // 2

m.delete("firstName"); // 只删除这一个键/值对
console.log(m.has("firstName")); // false
console.log(m.has("lastName"));  // true
console.log(m.size);             // 1

m.clear(); // 清除这个映射实例中的所有键/值对
console.log(m.has("firstName")); // false
console.log(m.has("lastName"));  // false
console.log(m.size);             // 0

set()方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明:

const m = new Map().set("key1", "val1");

m.set("key2", "val2")
  .set("key3", "val3");

console.log(m.size); // 3

与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。

顺序与迭代

与 Object 类型的一个主要差异是,Map 实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。

映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组。可以 通过 entries()方法(或者 Symbol.iterator 属性,它引用 entries())取得这个迭代器:

const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"]
]);

console.log(m.entries === m[Symbol.iterator]); // true;

for(let pair of m.entries()) {
  console.log(pair);
}
// [ 'key1', 'val1' ]
// [ 'key2', 'val2' ]
// [ 'key3', 'val3' ]


for(let pair of m[Symbol.iterator]()) {
  console.log(pair);
}
// [ 'key1', 'val1' ]
// [ 'key2', 'val2' ]
// [ 'key3', 'val3' ]

因为 entries()是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:

const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"]
]);

console.log([...m]); // [ [ 'key1', 'val1' ], [ 'key2', 'val2' ], [ 'key3', 'val3' ] ]

如果不使用迭代器,而是使用回调方式,则可以调用映射的forEach(callback,opt_thisArg) 方法并传入回调,依次迭代每个键/值对。传入的回调接收可选的第二个参数,这个参数用于重写回调 内部 this 的值:

const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"]
]);
m.forEach((val, key) => console.log(`${key} -> ${val}`));
// key1 -> val1
// key2 -> val2
// key3 -> val3

keys()values()分别返回以插入顺序生成键和值的迭代器:

const m = new Map([
  ["key1", "val1"],
  ["key2", "val2"],
  ["key3", "val3"]
]);

for (let key of m.keys()) { 
  console.log(key);
}
// key1
// key2
// key3

for (let key of m.values()) { 
  console.log(key);
}
// value1
// value2
// value3

选择 Object 还是 Map

  1. 内存占用

Map 比 Object 的内存占用要小

Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量 都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。 不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。

  1. 插入性能

向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快 一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳

  1. 查找速度

与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对, 则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言, 查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。

  1. 删除性能

使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此, 出现了一些伪删除对象属性的操作,包括把属性值设置为undefined或null。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。 如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。

WeakMap

ECMAScript 6 新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/ 值对存储机制。

WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集。WeakMap 中的“weak”(弱), 描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。

基本API

可以使用 new 关键字实例化一个空的 WeakMap:const wm = new WeakMap();

弱映射中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出 TypeError。值的类型没有限制。

如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键/值对数组。 可迭代对象中的每个键/值都会按照迭代顺序插入新实例中:

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

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

console.log(wm1.get(key1)); // val1
console.log(wm1.get(key2)); // val2
console.log(wm1.get(key3)); // val3
const key1 = {id:1},
      key2 = {id:2},
      key3 = {id:3};

// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败 
const wm2 = new WeakMap([
  [key1, "val1"], 
  ["BADKEY", "val2"], 
  [key3, "val3"]
]);
// TypeError: Invalid value used as WeakMap key

typeof wm2;
// ReferenceError: wm2 is not defined
// 原始值可以先包装成对象再用作键
const stringKey = new String("key1"); 
console.log(stringKey);
const wm3 = new WeakMap([
  [stringKey, "val1"],
]);
console.log(wm3.get(stringKey)); // "val1"

初始化之后可以使用 set()再添加键/值对,可以使用 get()has()查询,还可以使用 delete() 删除:

const wm = new WeakMap();

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

console.log(wm.has(key1)); // false
console.log(wm.get(key1)); // undefined

wm.set(key1, "Matt")
  .set(key2, "Frisbie");

console.log(wm.has(key1)); // true
console.log(wm.get(key1)); // Matt

wm.delete(key1); // 只删除这一个键/值对 

console.log(wm.has(key1)); // false
console.log(wm.has(key2)); // true

set()方法返回弱映射实例,因此可以把多个操作连缀起来,包括初始化声明:

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

const wm = new WeakMap().set(key1, "val1");

wm.set(key2, "val2")
      .set(key3, "val3");
    
console.log(wm.get(key1)); // val1
console.log(wm.get(key2)); // val2
console.log(wm.get(key3)); // val3

弱键

WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用, 不会阻止垃圾回收。

但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值 对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。

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(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以 把这个键/值对清理掉。

不可迭代键

因为 WeakMap 中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。

当然, 也用不着像 clear()这样一次性销毁所有键/值的方法。WeakMap 确实没有这个方法。因为不可能迭代, 所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问 WeakMap 实例,也没办法看到其中的内容。

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

Set

基本API

使用 new 关键字和 set 构造函数可以创建一个空集合:const m = new Set();

如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素:

// 使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"]);
console.log(s1); // Set(3) { 'val1', 'val2', 'val3' }

// 使用自定义迭代器初始化集合
const s2 = new Set({
  [Symbol.iterator]: function*() {
    yield "val1";
    yield "val2";
    yield "val3";
  }
});
console.log(s2); // Set(3) { 'val1', 'val2', 'val3' }

初始化之后,可以使用 add() 增加值,使用 has() 查询,通过 size 取得元素数量,以及使用 delete()clear() 删除元素:

const s = new Set();

console.log(s.has('Matt')); // false
console.log(s.size); // 0

s.add("Matt")
 .add("Frisbie");

console.log(s.has("Matt")); // true
console.log(s.size); // 2

s.delete("Matt");

console.log(s.has("Matt")); // false
console.log(s.has("Frisbie")); // true
console.log(s.size);           // 1

s.clear(); // 销毁集合实例中的所有值
console.log(s.has("Matt"));    // false
console.log(s.has("Frisbie")); // false
console.log(s.size);           // 0

add() 返回集合的实例,所以可以将多个添加操作连缀起来,包括初始化:

const s = new Set().add("val1");

s.add("val2")
 .add("val3");

console.log(s.size); // 3

Set 可以包含任何 JavaScript 数据类型作为值。

顺序与迭代

Set 会维护值插入时的顺序,因此支持按顺序迭代。

集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容。可以通过 values() 方 法及其别名方法 keys() (或者 Symbol.iterator 属性,它引用 values())取得这个迭代器:

const s = new Set(['val1', 'val2', 'val3']);

console.log(s.values === s[Symbol.iterator]); // true
console.log(s.keys === s[Symbol.iterator]); // true

for(let value of s.values()) {
  console.log(value);
}
// val1
// val2
// val3

for(let value of s[Symbol.iterator]()) {
  console.log(value);
}
// val1
// val2
// val3

因为 values()是默认迭代器,所以可以直接对集合实例使用扩展操作,把集合转换为数组:

const s = new Set(["val1", "val2", "val3"]);
console.log([...s]); // ["val1", "val2", "val3"]

集合的 entries()方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元 素是集合中每个值的重复出现:

const s = new Set(["val1", "val2", "val3"]);

for (let pair of s.entries()) { 
  console.log(pair);
}
// ["val1", "val1"]
// ["val2", "val2"]
// ["val3", "val3"]

如果不使用迭代器,而是使用回调方式,则可以调用集合的 forEach()方法并传入回调,依次迭代每个键/值对。

const s = new Set(["val1", "val2", "val3"]);
s.forEach((val, dupVal) => console.log(`${val} -> ${dupVal}`));
// val1 -> val1
// val2 -> val2
// val3 -> val3

WeakSet

ECMAScript 6 新增的“弱集合”(WeakSet)是一种新的集合类型,为这门语言带来了集合数据结构。WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集。WeakSet 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱集合”中值的方式。

基本API

可以使用 new 关键字实例化一个空的 WeakSet:const ws = new WeakSet();

弱集合中的值只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置值会抛出 TypeError。

如果想在初始化时填充弱集合,则构造函数可以接收一个可迭代对象,其中需要包含有效的值。可 迭代对象中的每个值都会按照迭代顺序插入到新实例中:

const val1 = {id: 1},
      val2 = {id: 2},
      val3 = {id: 3};

// 使用数组初始化弱集合
const ws1 = new WeakSet([val1, val2, val3]);
console.log(ws1.has(val1)); // true
console.log(ws1.has(val2)); // true
console.log(ws1.has(val3)); // true


// 初始化是全有或全无的操作
// 只要有一个值无效就会抛出错误,导致整个初始化失败
const ws2 = new WeakSet([val1, "BADVAL", val3]); 
// TypeError: Invalid value used in WeakSet typeof ws2;
// ReferenceError: ws2 is not defined

// 原始值可以先包装成对象再用作值
const stringVal = new String("val1"); 
const ws3 = new WeakSet([stringVal]); 
console.log(ws3.has(stringVal)); // true

初始化之后可以使用 add() 再添加新值,可以使用 has() 查询,还可以使用 delete() 删除:

const ws = new WeakSet();

const val1 = {id: 1},
      val2 = {id: 2};

console.log(ws.has(val1)); // false

ws.add(val1)
  .add(val2);

console.log(ws.has(val1)); // true
console.log(ws.has(val2)); // true

ws.delete(val1); // 只删除这一个值 
console.log(ws.has(val1)); // false
console.log(ws.has(val2)); // true

add() 方法返回弱集合实例,因此可以把多个操作连缀起来,包括初始化声明:

const val1 = {id: 1},
      val2 = {id: 2},
      val3 = {id: 3};

const ws = new WeakSet().add(val1);
ws.add(val2)
  .add(val3);

console.log(ws.has(val1)); // true
console.log(ws.has(val2)); // true
console.log(ws.has(val3)); // true

弱值

WeakSet 中“weak”表示弱集合的值是“弱弱地拿着”的。意思就是,这些值不属于正式的引用,不会阻止垃圾回收。

const ws = new WeakSet();
ws.add({});

add()方法初始化了一个新对象,并将它用作一个值。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收。然后,这个值就从弱集合中消失了,使其成为 一个空集合。

const ws = new WeakSet();
const container = {
  val: {}
};
ws.add(container.val);
function removeReference() {
  container.val = null;
}

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

不可迭代值

因为 WeakSet 中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力。当然,也用不着像 clear()这样一次性销毁所有值的方法。WeakSet 确实没有这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱集合中取得值。即便代码可以访问 WeakSet 实例,也没办法看到其中的内容。

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

总结:

Map 是对 Object 的扩展,与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。

Map 可以使用 API 进行操作, set() get() has() delete()和 clear() 支持顺序迭代。

WeakMap 是 Map 的子集, 键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出 TypeError。值的类型没有限制。
键不属于正式的引用, 不会阻止垃圾回收。没有指向这个对象的其他引用,对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失 了,使其成为一个空映射。
WeakMap 中的键/值对任何时候都可能被销毁没有迭代的功能。
也没有clear()这样一次性销毁所有键/值的方法。
访问 WeakMap 实例,也没办法看到其中的内容。

Set 可以包含任何 JavaScript 数据类型作为值。 可以使用 API 进行操作, add() get() has() delete()和 clear() 支持顺序迭代。

WeakSetWeakMap 类似。