2

Map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

Map的构造

ES6 的 Map 类型是键值对的有序列表,而键和值都可以是任意类型。 键的比较使用的是Object.is() ,因此你能将 5 与 "5"
同时作为键,因为它们类型不同。这与使用对象属性作为键的方式(指的是用对象来模拟 Map )截然不同,因为对象的属性会被强制转换为字符串。

set方法构造

你可以调用 set() 方法并给它传递一个键与一个关联的值,来给 Map 添加项;此后使用键名来调用 get() 方法便能提取对应的值。例如:

let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2016);
console.log(map.get("title")); // "Understanding ES6"
console.log(map.get("year")); // 2016

数组构造

依然与 Set 类似,你能将数组传递给 Map 构造器,以便使用数据来初始化一个 Map 。该数组中的每一项也必须是数组,内部数组的首个项会作为键,第二项则为对应值。因此整个Map 就被这些双项数组所填充。例如:

let map = new Map([["name", "Nicholas"], ["age", 25]]);
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.size); // 2

通过构造器中的初始化, "name" 与 "age" 这两个键就被添加到 map 变量中。虽然由数组构成的数组看起来有点奇怪,这对于准确表示键来说却是必要的:因为键允许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 之前不会被强制转换为其他类型的唯一方法。
Map构造函数接受数组作为参数,实际上执行的是下面的算法。

const items = [
["name", "Nicholas"]
  ["age", 25] ]
  
  const map = new Map();
  
  items.forEach(
    ([key, value]) => map.set(key, value)
  );

事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当做Map构造函数的参数。这就是说,Set和Map都可以用来生成新的Map

其他构造

 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

上面代码中,我们分别使用 Set 对象和 Map 对象,当作Map构造函数的参数,结果都生成了新的 Map 对象。

Map的属性和方法

1、属性

Map 同样拥有 size 属性,用于指明包含了多少个键值对

2、方法

2.1、操作方法
2.11、set(key, value)

set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

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

set方法返回的是当前的Map对象,因此可以采用链式写法。

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

get方法读取key对应的键值,如果找不到key,返回undefined。

const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数

m.get(hello)  // Hello ES6!
2.13、has、delete、clear
Map 与 Set 共享了几个方法,这是有意的,允许你使用相似的方式来与 Map 及 Set 进行交互。以下三个方法在 Map 与 Set
上都存在:
2.13.1、has(key)

has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true
2.13.2、delete(key)

delete方法删除某个键,返回true。如果删除失败,返回false。

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

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

clear方法清除所有成员,没有返回值。

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

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

2.2、遍历方法

2.2.1遍历器生成函数

• keys():返回键名的遍历器。
• values():返回键值的遍历器。
• entries():返回所有成员的遍历器

需要特别注意的是,Map 的遍历顺序就是插入顺序。

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

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

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "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"

上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

2.2.2、forEach

Map 的 forEach() 方法类似于 Set 与数组的同名方法,它接受一个能接收三个参数的回调函数:

  1. Map 中下个位置的值;
  2. 该值所对应的键;
  3. 目标 Map 自身。

回调函数的这些参数更紧密契合了数组 forEach() 方法的行为,即:第一个参数是值、第二个参数则是键(数组中的键是数值索引)。此处有个示例:

let map = new Map([
  ["name", "Nicholas"], 
  ["age", 25]
]);
map.forEach(function(value, key, ownerMap) {
  console.log(key + " " + value);
  console.log(ownerMap === map);
});

forEach() 的回调函数输出了传给它的信息。其中 value 与 key 被直接输出, ownerMap
与 map 进行了比较,说明它们是相等的。这就输出了:

name Nicholas
true
age 25
true    

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

Map转数组:

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

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

将数组传入 Map 构造函数,就可以转为 Map。

new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }

Map 转为对象

如果所有 Map 的键都是字符串,它可以无损地转为对象。

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

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

对象转为 Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}

Map 转为 JSON

Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

JSON 转为 Map

JSON 转为 Map,正常情况下,所有键名都是字符串。

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。

  function jsonToMap(jsonStr) {
      return new Map(JSON.parse(jsonStr));
    }

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}

WeakMap:

WeakMap的特性

WeakMap与Map的区别有两点。
首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

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

其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。

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

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

arr [0] = null;
arr [1] = null;

上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。

WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

WeakMap的构造:

ES6 的 WeakMap 类型是键值对的无序列表,其中键必须是非空的对象,值则允许是任意类型。 WeakMap 的接口与 Map 的非常相似
// WeakMap 可以使用 set 方法添加成员

const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也可以接受一个数组,
// 作为构造函数的参数

const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

WeakMap的属性和方法:

WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。

const wm = new WeakMap();
// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined

WeakMap常用场景

Weak Map 的最佳用武之地,就是在浏览器中创建一个关联到特定 DOM 元素的对象。例如,某些用在网页上的 JS 库会维护一个自定义对象,用于引用该库所使用的每一个 DOM 元素,并且其映射关系会存储在内部的对象缓存中。
该方法的困难之处在于:如何判断一个 DOM 元素已不复存在于网页中,以便该库能移除此元素的关联对象。若做不到,该库就会继续保持对 DOM 元素的一个无效引用,并造成内存泄漏。使用 Weak Map 来追踪 DOM 元素,依然允许将自定义对象关联到每个 DOM 元素,而在此对象所关联的 DOM 元素不复存在时,它就会在 Weak Map 中被自动销毁。
必须注意的是, Weak Map 的键才是弱引用,而值不是。在 Weak Map 的值中存储对象会阻止垃圾回收,即使该对象的其他引用已全都被移除。

当决定是要使用 Weak Map 还是使用正规 Map 时,首要考虑因素在于你是否只想使用对象类型的键。如果你打算这么做,那么最好的选择就是
Weak Map 。因为它能确保额外数据在不再可用后被销毁,从而能优化内存使用并规避内存泄漏。 要记住 Weak Map
只为它们的内容提供了很小的可见度,因此你不能使用 forEach() 方法、size 属性或 clear()
方法来管理其中的项。如果你确实需要一些检测功能,那么正规 Map会是更好的选择,只是一定要确保留意内存的使用。

晨楠而突然
8 声望0 粉丝

« 上一篇
es6 Set和WeakSet