ES6 Map、WeakMap详解

概述

如果你的 JavaScript 经验丰富的话,应该会了解对象是创建无序键/值对数据结构 [也称为 映射(map)] 的主要机制。但是,对象作为映射的主要缺点是不能使用非字符串值作为键。

举例来说,考虑:

1
2
3
4
5
6
7
8
9
var m = {};
var x = { id: 1 },
y = { id: 2 };

m[x] = "foo";
m[y] = "bar";

m[x]; // "bar"
m[y]; // "bar"

这里发生了什么? xy 两个对象字符串化都是 "[object Object]",所以 m 中只设置了一个键。

Map

在ES6中引入了 Map(..):

1
2
3
4
5
6
7
8
9
10
var m = new Map():

var x = { id: 1 },
y = { id: 2 };

m.set( x, "foo" );
m.set( y, "bar" );

m.get( x ); // "foo"
m.get( y ); // "bar"

这里唯一的缺点就是不能使用方括号 [ ] 语法设置和获取值,但完全可以使用 get(..)set(..) 方法完美代替。

要从 map 中删除一个元素,不要使用 delete 运算符,而是要使用 delete() 方法:

1
2
3
4
m.set( x, "foo" );
m.set( y, "bar" );

m.delete( y );

你可以通过 clear() 清除整个 map 的内容。要得到 map 的长度(也就是键的个数),可以 使用 size 属性(而不是 length):

1
2
3
4
5
6
m.set( x, "foo" );
m.set( y, "bar" );
m.size; // 2

m.clear();
m.size; // 0

Map(..) 构造器也可以接受一个 iterable,这个迭代器必须产生一列数组,每个数组的第一个元素是键,第二个元素是值。这种迭代的形式和 entries() 方法产生的形式是完全一样的,这使得创建一个 map 的副本很容易:

1
2
3
4
var m2 = new Map( m.entries() );

// 等价于:
var m2 = new Map( m );

因为 map 的实例是一个 iterable,它的默认迭代器与 entries() 相同,所以我们更推荐使用 后面这个简短的形式。
当然,也可以在 Map(..) 构造器中手动指定一个项目(entry)列表(键 / 值数组的数组):

1
2
3
4
5
6
7
8
9
10
var x = { id: 1 },
y = { id: 2 };

var m = new Map([
[ x, "foo" ],
[ y, "bar"]
]);

m.get( x ); // "foo"
m.get( y ); // "bar"

Map取值

要从map 中得到一列值,可以使用 values(..),它会返回一个迭代器。比如spread 运算符 … 和 for..of 循环。另外, Array.from(..) 方法。考虑:

1
2
3
4
5
6
7
8
9
10
11
12
var m = new Map();

var x = { id: 1 },
y = { id: 2 };

m.set( x, "foo" );
m.set( y, "bar" );

var vals = [ ...m.values() ];

vals; // ["foo","bar"]
Array.from( m.values() ); // ["foo","bar"]

可以在一个 map 的项目上使用 entries() 迭代(或者默认 map 迭代器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var m = new Map();

var x = { id: 1 },
y = { id: 2 };

m.set( x, "foo" );
m.set( y, "bar" );

var vals = [ ...m.entries()];

vals[0][0] === x; // true
vals[0][1]; // "foo"

vals[1][0] === y; // true
vals[1][1]; // "bar"

Map键值

要得到一列键,可以使用 keys(),它会返回 map 中键上的迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
var m = new Map();

var x = { id: 1 },
y = { id: 2 };

m.set( x, "foo" );
m.set( y, "bar" );

var keys = [ ...m.keys()];

keys[0] === x; // true
keys[1] === y; // true

要确定一个 map 中是否有给定的键,可以使用 has(..)方法:

1
2
3
4
5
6
7
8
9
var m = new Map();

var x = { id: 1 },
y = { id: 2 };

m.set( x, "foo" );

m.has( x ); // true
m.has( y ); // false

map 的本质是允许你把某些额外的信息(值)关联到一个对象(键)上,而无需把这个信 息放入对象本身。
对于 map 来说,尽管可以使用任意类型的值作为键,但通常我们会使用对象,因为字符串 或者其他基本类型已经可以作为普通对象的键使用。换句话说,除非某些或者全部键需要是对象,否则可以继续使用普通对象作为影射,这种情况下 map 才更加合适。

  • Tips:如果使用对象作为映射的键,这个对象后来被丢弃(所有的引用解除),试图让垃圾回收(GC)回收其内存,那么 map 本身仍会保持其项目。你需要 从 map 中移除这个项目来支持 GC,这时候就需要 WeakMap

WeakMap

WeakMapmap 的变体,二者的多数外部行为特性都是一样的,区别在于内部内存分配 (特别是其 GC)的工作方式。
WeakMap(只)接受对象作为键。这些对象是被弱持有的,也就是说如果对象本身被垃圾回收的话,在 WeakMap 中的这个项目也会被移除。然而我们无法观测到这一点,因为对象被垃圾回收的唯一方式是没有对它的引用了。但是一旦不再有引用,你也就没有对象引 用来查看它是否还存在于这个 WeakMap 中了。

除此之外,WeakMap 的 API 是类似的,尽管要更少一些:

1
2
3
4
5
6
7
8
9
var m = new WeakMap();

var x = { id: 1 },
y = { id: 2 };

m.set( x, "foo" );

m.has( x ); // true
m.has( y ); // false

WeakMap 没有 size 属性或 clear() 方法,也不会暴露任何键、值或项目上的迭代器。所以即使你解除了对 x 的引用,它将会因 GC 时这个条目被从 m 中移除,也没有办法确定这一事实。所以你就相信 JavaScript 所声明的吧

Map 一样,通过 WeakMap 可以把信息与一个对象软关联起来。而在对这个对象没有完 全控制权的时候,这个功能特别有用,比如 DOM 元素。如果作为映射键的对象可以被删除,并支持垃圾回收,那么 WeakMap 就更是合适的选择了。
需要注意的是,WeakMap 只是弱持有它的键,而不是值。考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var m = new WeakMap();

var x = { id: 1 },
y = { id: 2 },
z = { id: 3 },
w = { id: 4 };

m.set( x, y );

x = null; // { id: 1 } 可GC
y = null; // { id: 2 } 可GC
// 只因 { id: 1 } 可GC

m.set( z, w );

w = null; // { id: 4 } 不可GC