第六章 JavaScript标准库 6.11 Map

37 阅读5分钟

6.11 Map

JavaScript 的对象本质上是一种键值对的集合,但是传统上只能使用字符串作为键,这限制了对象在某些场合下的使用。 一方面,只能使用字符串作为键使得对象不能够很好地满足一些需要使用其他类型作为键的需求。例如,如果想要以函数作为键来记录某些操作的执行情况,就不能直接使用对象来实现。 另一方面,由于对象的键都是字符串,如果多个属性名相同的键值对出现在一个对象中,就会出现覆盖的情况,这在某些场合下是不合适的。 为了解决这些问题,ES6 中引入了 Map 和 WeakMap 这两个新的数据结构。其中,Map 可以使用任何类型的值作为键,而 WeakMap 则提供了一种弱引用的数据结构,可以避免因为使用对象作为键而导致的内存泄漏问题。

Map 构造函数

Map 的构造函数如下: new Map([iterable]) 其中可选参数 iterable 是一个可迭代对象,它可以是一个数组或任何实现了迭代器协议的对象。如果提供了 iterable 参数,则 Map 会将其转化为一个由键值对组成的数组。每个键值对都是一个数组,其中第一个元素为键,第二个元素为值。例如:

const myArray = [
  ['key1', 'value1'],
  ['key2', 'value2'],
];
const myMap = new Map(myArray);

上述代码中,myMap 对象将包含两个键值对:'key1' => 'value1' 和 'key2' => 'value2'。

Map 实例方法

Map 实例对象具有以下常用的方法和 Set 对象基本一致,方法如下: 1 Map.prototype.set(key, value):设置键名 key 对应的键值为 value,并返回该 Map 对象。如果该键名已经存在,则会更新该键名对应的键值。 2 Map.prototype.get(key):获取键名 key 对应的键值,如果该键名不存在,则返回 undefined。 3 Map.prototype.has(key):判断 Map 对象中是否存在键名为 key 的键值对,如果存在则返回 true,否则返回 false。 4 Map.prototype.delete(key):删除键名为 key 的键值对,并返回一个布尔值表示是否删除成功。如果删除成功,则返回 true,否则返回 false。 5 Map.prototype.clear():清空该 Map 对象中的所有键值对。 6 Map.prototype.size:返回该 Map 对象中键值对的数量。 7 Map.prototype.keys():返回一个包含该 Map 对象中所有键名的迭代器对象。 8 Map.prototype.values():返回一个包含该 Map 对象中所有键值的迭代器对象。 9 Map.prototype.entries():返回一个包含该 Map 对象中所有键值对的迭代器对象。 10 Map.prototype.forEach(callbackFn, thisArg):使用回调函数 callbackFn 遍历该 Map 对象中的所有键值对。callbackFn 函数接受三个参数:当前键值对的值 value、当前键值对的键名 key、以及 Map 对象本身。可以通过第二个可选参数 thisArg 指定回调函数中 this 的值。 下面列举一些使用这些方法的例子,更详细的介绍可以参考 Set 章节:

// 创建一个新的 Map 实例
const myMap = new Map();

// 添加键值对
myMap.set('key1', 'value1');
myMap.set('key2', 'value2');
myMap.set('key3', 'value3');

// 获取键值对的数量
console.log(myMap.size); // 3

// 通过键获取值
console.log(myMap.get('key2')); // value2

// 检查是否包含某个键
console.log(myMap.has('key4')); // false

// 删除某个键值对
myMap.delete('key3');

// 检查某个键值对是否被删除
console.log(myMap.has('key3')); // false

// 清空 Map 实例
myMap.clear();

// 遍历 Map 实例
myMap.set('key4', 'value4');
myMap.set('key5', 'value5');
myMap.forEach(function (value, key) {
  console.log(key + ' = ' + value);
});

// 使用迭代器遍历 Map 实例的键值对
for (let [key, value] of myMap) {
  console.log(key + ' = ' + value);
}

Map 的键值比较

Map 和对象的键值对最大的区别就是对象的 key 值只能是字符串,所以后者判断是否是同一个键值的依据就是字符串的比较,例如:

const obj = {};
obj[1] = 'value1';
obj['1'] = 'value2';

console.log(obj[1]); // value2
console.log(obj['1']); // value2

以上例子因为 number 1 作为键值 会转换成字符串 1,所以后边设置的 value2 会覆盖 value1。 下面看使用 Map 的情况:

const myMap = new Map();
myMap.set(1, 'value1');
myMap.set('1', 'value2');
console.log(myMap.get(1)); // "value1"
console.log(myMap.get('1')); // "value2"

可以看到 Map 中数字 1 和字符串 1 的 key 值是不同的。 Map 对象的键值比较,总结起来就是 如果 Map 的键是一个简单类型的值(数字、字符串、布尔值等),则只要它们严格相等(===),Map 就将它们视为同一个键,NaN 是一个特例,即使不严格相等 但是视喂同一个键。如果 Map 的键是引用类型,则基于内存地址是否相同判断是否是同一个键。 简单类型的例子:

const myMap = new Map();

myMap.set(1, 'value1');
myMap.set('1', 'value2');
myMap.set(true, 'value3');
myMap.set(true, 'value3-2');
myMap.set(false, 'value4');
myMap.set(false, 'value4-2');
myMap.set(null, 'value5');
myMap.set(null, 'value5-2');
myMap.set(undefined, 'value6');
myMap.set(undefined, 'value6-2');
myMap.set(NaN, 'value7');
myMap.set(NaN, 'value7-2');

console.log(myMap.get(1)); // "value1"
console.log(myMap.get('1')); // "value2"
console.log(myMap.get(true)); // "value3-2"
console.log(myMap.get(false)); // "value4-2"
console.log(myMap.get(null)); // "value5-2"
console.log(myMap.get(undefined)); // "value6-2"
console.log(myMap.get(NaN)); // "value7-2"
console.log(myMap.size); // 7

引用类型的例子:
const myMap = new Map();

const key1 = {};
const key2 = {};

myMap.set(key1, 'value1');
myMap.set(key2, 'value2');

console.log(myMap.get(key1)); // "value1"
console.log(myMap.get(key2)); // "value2"

console.log(myMap.get({})); // undefined
console.log(myMap.get({})); // undefined

console.log(key1 === key2); // false
console.log({} === {}); // false

在这个例子中,我们创建了两个对象 key1 和 key2,并将它们作为 Map 的键存储了两个不同的值。然后,我们使用 get() 方法分别获取了这两个键所对应的值,结果正确。接下来,我们又使用了两个新创建的对象作为键,但是它们并没有在 Map 中找到对应的值,因为它们的内存地址与 key1 和 key2 不同。最后,我们比较了 key1 和 key2,以及两个新创建的空对象,发现它们的内存地址都不同,因此都被视为不同的键。