Skip to content
On this page

Map-WeakMap

1. Map

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

也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。

1.1. 构造函数

Map本身是一个构造函数,用来生成Map数据结构。

Map接受一个数组作为参数(具有Iterator接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数)

js
const items = [
  ['name', '张三'],
  ['title', 'Author']
]
const map = new Map()
map.get('name') // "张三"
map.get('title') // "Author"

实际上,上面的代码写法等同于下面的算法

js
const items = [
  ['name', '张三'],
  ['title', 'Author']
];
const map = new Map();
items.forEach(([key, value]) => map.set(key, value));

由于只要具有Iterator接口且每个成员都是一个双元素的数据结构都可以作为Map的参数,所以SetMap都可以用来生成新的Map

js
const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

1.2. 属性

constructor

Map.prototype.constructor,构造函数,默认就是Map函数。

size

Map.prototype.size,返回Map实例的成员总数。

js
onst map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2

1.3. 方法

Map实例的方法分为两类: 操作方法遍历方法

操作方法

set()

Map.prototype.set(key, value),添加键名为key,对应的值为value的成员,返回Map结构本身。

如果key已经有值,则键值会被更新(即重复添加会被覆盖),否则就新生成该键。

js
const m = new Map();
m.set('edition', 6)        // 键是字符串
m.set(262, 'standard')     // 键是数值
m.set(undefined, 'nah')    // 键是 undefined

由于set方法返回的是Map结构本身,所以我们可以通过链式调用。

js
let map = new Map()
map.set(1, 'a').set(2, 'b').set(3, 'c');
get()

Map.prototype.get(key),读取key对应的键值,如果找不到key,返回undefined

js
const m = new Map();
m.set('name', '张三')
m.get('name') // 张三
m.get('age') // undefined
has()

Map.prototype.has(key),返回一个布尔值,表示某个键是否在当前Map对象之中。

js
const m = new Map();
m.set('edition', 6);

m.has('edition') // true
m.has(262) // false
delete()

Map.prototype.delete(key),删除某个值,返回一个布尔值,表示删除是否成功。

js
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true

m.delete(undefined)
m.has(undefined) // false
clear()

Map.prototype.clear(),清除所有成员,没有返回值。

js
const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0

遍历方法

注意: Map的遍历顺序就是插入顺序。

keys()

Map.prototype.keys(),返回键名的遍历器。

js
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"
values()

Map.prototype.values(),返回键值的遍历器。

js
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"
entries()

Map.prototype.entries(),返回所有成员的遍历器。

js
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);
for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"
forEach()

Map.prototype.forEach(),与数组的forEach方法类似,也可以实现遍历。

js
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);
map.forEach((value, key, map) => {
  console.log(value, key, value);
});

forEach方法还可以接受第二个参数,用来绑定this

js
const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};
map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);

1.4. 应用

扩展运算符

Map结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

js
const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()] // [1, 2, 3]

[...map.values()] // ['one', 'two', 'three']

[...map.entries()] // [[1,'one'], [2, 'two'], [3, 'three']]

[...map] // [[1,'one'], [2, 'two'], [3, 'three']]
map和filter

结合数组的map方法、filter方法,可以实现Map的遍历和过滤(Map本身没有mapfilter方法)。

js
const map0 = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

const map1 = new Map(
  [...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}

const map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}

2. WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。区别在于:

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
js
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
  1. WeakMap的键名所指向的对象,不计入垃圾回收机制

WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。

js
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];

上面代码中,e1和e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arr对e1和e2的引用。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1和e2占用的内存。

js
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;

WeakMap就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。

注意: WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用。

2.1. 构造函数

WeakMap是一个构造函数,可以使用new命令,创建WeakMap数据结构。

js
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}

2.2. 属性

constructor

WeakMap.prototype.constructor,构造函数,默认就是WeakMap函数。

注意

WeakMap没有size属性,没有办法遍历它的成员。同时也没有forEachclear方法。

2.3. 方法

set()

WeakMap.prototype.set(key, value),添加键名为key,对应的值为value的成员,返回WeakMap结构本身。

js
const wm = new WeakMap()
let key = {};
let obj = {foo: 1};
wm.set(key, obj);

由于set方法返回的是WeakMap结构本身,所以我们可以通过链式调用。

js
const wm = new WeakMap()
let key = {};
let key2 = {};
let obj = {foo: 1};
wm.set(key, obj).set(key1, obj);

get()

WeakMap.prototype.get(key),读取key对应的键值,如果找不到key,返回undefined

js
const wm = new WeakMap()
let key = {};
let obj = {foo: 1};
wm.set(key, obj);

wm.get(key) // { foo: 1 }

has()

WeakMap.prototype.has(key),返回一个布尔值,表示某个键是否在当前WeakMap对象之中。

js
const wm = new WeakMap()
let key = {};
let obj = {foo: 1};
wm.set(key, obj);

wm.has(key)

delete()

WeakMap.prototype.delete(key),删除某个值,返回一个布尔值,表示删除是否成功。

js
const wm = new WeakMap()
let key = {};
let obj = {foo: 1};
wm.set(key, obj);

wm.delete(key)

2.4 应用

WeakMap应用的典型场合就是 DOM 节点作为键名。

js
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);

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

WeakMap的另一个用处是部署私有属性。

js
const _counter = new WeakMap();
const _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

const c = new Countdown(2, () => console.log('DONE'));

c.dec()
c.dec()
// DONE

上面代码中,Countdown类的两个内部属性_counter_action,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。

3. WeakRef

WeakSetWeakMap是基于弱引用的数据结构,ES2021 更进一步,提供了WeakRef对象,用于直接创建对象的弱引用。

js
let target = {};
let wr = new WeakRef(target);

面示例中,target是原始对象,构造函数WeakRef()创建了一个基于target的新对象wr。这里,wr就是一个WeakRef的实例,属于对target的弱引用,垃圾回收机制不会计入这个引用,也就是说,wr的引用不会妨碍原始对象target被垃圾回收机制清除。

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

js
let target = {};
let wr = new WeakRef(target);

let obj = wr.deref();
if (obj) { // target 未被垃圾回收机制清除
  // ...
}

上面示例中,deref()方法可以判断原始对象是否已被清除。

弱引用对象的一大用处,就是作为缓存,未被清除时可以从缓存取值,一旦清除缓存就自动失效。

js
function 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;
  };
}

const getImageCached = makeWeakCached(getImage);

上面示例中,makeWeakCached()用于建立一个缓存,缓存里面保存对原始文件的弱引用。

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

Released under the MIT License.