糯麦 NurMai

400-158-5662

糯麦科技

/

新闻资讯

/

技术讨论

/

彻底弄懂JS中Map与Object之间的区别已经高级用法。

彻底弄懂JS中Map与Object之间的区别已经高级用法。

原创 新闻资讯

于 2023-08-24 10:16:40 发布

14643 浏览

之前,我们初步探讨了JavaScript中的Map对象,介绍了其基本概念和用法。如果你错过了那篇文章,强烈建议你现在回去阅读一下,这将有助于你更好地理解我们今天要分享的内容。在今天的分享中,我们将继续探讨Map对象,重点介绍其遍历方法、高级用法,以及它与Object的区别。

对了,你更倾向于使用Map还是Object呢?在项目中,它们是否曾经给你带来过困扰?如果你还没有确定的答案,希望接下来的文章能帮助你找到答案。

现在,让我们开始吧!


1. 如何遍历 Map

遍历 Map 对象是一个非常常见的操作,JS 为我们提供了哪些方法呢?


1.1 keys() 方法

该方法返回一个新的迭代器对象,它包括 Map 对象中每个元素的键。

const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);

for (const key of map.keys()) {
  console.log(key);
}
// 输出:'name' 'age'


1.2 values() 方法

该方法返回一个新的迭代器对象,它包括 Map 对象中每个元素的值。

const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);

for (const value of map.values()) {
  console.log(value);
}
// 输出:'Alice' 25


1.3 entries() 方法

该方法返回一个新的迭代器对象,它包括 Map 对象中每个元素的键值对。

const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);

for (const [key, value] of map.entries()) {
  console.log(key + ': ' + value);
}
// 输出:'name: Alice' 'age: 25'


1.4 forEach() 方法

此方法接受一个回调函数作为参数,Map 对象中的每个元素都会调用一次这个回调函数。回调函数中的参数依次为:value、key、mapObject。

const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);

map.forEach((value, key) => {
  console.log(key, value);
});
// 输出:'name' 'Alice' 'age' 25

上面提到的这四个方法都可以用来遍历 Map 对象,你可以根据实际需求选择合适的方法。


2. Map 对象的进阶用法

了解了如何遍历 Map 对象后,我们来看一下 Map 都有哪些高级用法:


2.1 Map 和 Array 的相互转化

有时我们需要在 Map 和 Array 之间相互转化,比如将数组转换为字典,或者从字典转换为数组时,我们就可以结合 Array 构造函数和扩展运算符来实现。

let kvArray = [['key1', 'value1'], ['key2', 'value2']];
let myMap = new Map(kvArray);
console.log(myMap); // Map(2) {"key1" => "value1", "key2" => "value2"}

let arrayFromMap = Array.from(myMap);
console.log(arrayFromMap); // [["key1", "value1"], ["key2", "value2"]]


2.2 Map 的合并和复制

当需要将多个映射结构合并为一个,或者在需要复制 Map 对象时,可以用 Map 对象的 set() 方法和扩展运算符来实现。比如,你创建了一个 Map 对象,然后通过 forEach() 方法和 set() 方法将一个 Map 对象的所有键值对都复制到一个新的 Map 对象中。当然,你也可以用扩展运算符将多个 Map 对象合并为一个新的 Map 对象。

let map1 = new Map().set('a', 1).set('b', 2);
let map2 = new Map().set('c', 3).set('d', 4);

// 合并
let merged = new Map([...map1, ...map2]);
console.log(merged); // Map(4) {"a" => 1, "b" => 2, "c" => 3, "d" => 4}

// 复制
let copied = new Map(merged);
console.log(copied); // Map(4) {"a" => 1, "b" => 2, "c" => 3, "d" => 4}

不过,需要注意的是,通过扩展运算符和 new Map 复制 Map 对象的方式是浅拷贝。也就是说,如果 Map 对象的键或值是一个对象或者数组,那么复制后的 Map 对象中这个键或值将与原来 Map 中的相同,他们引用的是同一个对象或数组。这时,如果你修改了复制后的 Map 对象中的这个对象或数组,原来 Map 对象中相同对象或数组也会被修改。

let originalMap = new Map();
let objKey = {id: 1};
let objValue = {name: 'value'};
originalMap.set(objKey, objValue);

let copiedMap = new Map(originalMap);
console.log(copiedMap.get(objKey) === objValue); // true

// 修改复制的 Map 中的对象
copiedMap.get(objKey).name = 'new value';
console.log(originalMap.get(objKey).name); // "new value"

假如你要实现深拷贝,可以结合 JSON 方法来实现。但是要注意,使用 JSON 方法只能用于键和值都可以被 JSON 序列化的情况。

let originalMap = new Map();
let objKey = {id: 1};
let objValue = {name: 'value'};
originalMap.set(JSON.stringify(objKey), objValue);

let deepCopiedMap = new Map();
for (let [key, value] of originalMap) {
  deepCopiedMap.set(JSON.parse(key), JSON.parse(JSON.stringify(value)));
}

console.log(deepCopiedMap.get(JSON.parse(JSON.stringify(objKey))) === objValue); // false

// 修改深拷贝的 Map 中的对象
deepCopiedMap.get(JSON.parse(JSON.stringify(objKey))).name = 'new value';
console.log(originalMap.get(JSON.stringify(objKey)).name); // "value"

因 JSON 方法对 Map 的键和值有较多的限制,若要实现更为通用的 Map 的深拷贝,你可能要自己实现深拷贝函数,或者使用第三方库也行,比如 lodash 的 _.cloneDeep() 方法。


2.3 Map 的大小

要获取 Map 的大小,可以直接通过 Map 上的 size 属性来实现。相对于 Object,这是 Map 对象的一大优势。要知道,在 Object 中你需要手动计算属性的数量。

let map = new Map().set('a', 1).set('b', 2);
console.log(map.size); // 2


2.4 有序性

Map 对象会保留键值对的插入顺序,这是 Object 所不具备的。比如,需要对键值对进行排序或迭代的场景,Map 的这个特性就比 Object 更为有用。

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

for (let key of map.keys()) {
  console.log(key);
}
// 'a'
// 'b'
// 'c'


2.5 弱映射

JS 提供了一种特殊类型的 Map——WeakMap,其键必须是对象,且对键的引用是弱引用。WeakMap 特别适用于保存对其键的临时引用,这种弱引用不会阻止 JS 的垃圾回收器的自动清理。

let weakmap = new WeakMap();
let obj = {};

weakmap.set(obj, 'hello');
console.log(weakmap.get(obj)); // "hello"

obj = null;
// obj 被清理, weakmap 现在不包含任何元素


3. Map 与 Object 的比较

了解了 Map 对象的高级用法后,我们来看看 Map 对象与 Object 对象之间的主要区别:


3.1 键的类型

我们知道,JS 对象中的键只能是字符串或符号,而 Map 的键可以是任何类型。

// 使用 Object
let obj = {};
obj[5] = 'foo';
console.log(obj['5']);  // 'foo', 因为键 '5' 和 '5' 在 Object 中被认为是相同的。

// 使用 Map
let map = new Map();
map.set(5, 'foo');
console.log(map.get(5));  // 'foo'
console.log(map.get('5'));  // undefined, 因为 5 和 '5' 在 Map 中被认为是不同的键。

在这个示例中,我们分别演示了将指定的键添加到 Object 和 Map 中。通过观察可以发现,在 Object 中,所有非符号键都会被转换为字符串。所以呢,如果你这时候通过数字键检索值时,实际上使用的是字符串来检索值的。


再者,Map 允许你使用任何类型的键,这就意味着 5 和 '5' 被视为不同的键。在实际应用中,这种特性使得 Map 可以存储更加复杂和多样化的数据结构。比如,你可以使用对象、数组或甚至函数作为 Map 的键,而这在 Object 中却是无法实现的


3.2 键的顺序

Map 对象会保持键值对的插入顺序,而 Object 对象则不会。

// 使用 Object
let obj = {
  'b': 'foo',
  'a': 'bar'
};
console.log(Object.keys(obj));  // ['b', 'a'], Object 不保证键的顺序。

// 使用 Map
let map = new Map();
map.set('b', 'foo');
map.set('a', 'bar');
console.log([...map.keys()]);  // ['b', 'a'], Map 保持键的插入顺序。

在向 Object 中添加键值对时,Object 不能保证返回的键的顺序与插入时保持一致,但是 Map 一定会按照插入时的顺序原封不动的返回。一个典型的场景,假如要实现一个有序的字典或列表时,Map 的这种有序性将会非常有用。


3.3 性能

在频繁添加和删除键值对的操作中,Map 的性能通常优于 Object。

// 使用 Object
let obj = { 'a': 'foo', 'b': 'bar', 'c': 'baz' };
delete obj.b;
console.log(obj);  // { 'a': 'foo', 'c': 'baz' }, 删除操作可能会影响 Object 的性能。

// 使用 Map
let map = new Map();
map.set('a', 'foo').set('b', 'bar').set('c', 'baz');
map.delete('b');
console.log([...map]);  // [['a', 'foo'], ['c', 'baz']], Map 对象在频繁添加和删除键值对的操作中性能更优。

其实,在数据量较小或者没有频繁添加、删除键值对时,Object 和 Map 的性能差距可以忽略不计。但是,如果要操作的数据量非常大,或者很频繁的添加、删除键值对,这时候还是要考虑使用 Map 的。比如,在缓存或者其他一些动态数据集合中,Map 要比 Object 更有优势。


4. Map 对象实例分析

现在你应该对 Map 对象有了比较深入的理解,下面我们再通过两个示例来展示 Map 对象在实际问题中的应用。


4.1 使用 Map 实现 LRU Cache(最近最少使用页面替换算法)

LRU Cache 是一种常见的缓存策略,它可以把最近最少使用的数据给淘汰掉。下面是一个简单的示例:

class LRUCache {
  constructor(maxSize) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  get(key) {
    if(!this.cache.has(key)) return -1;
    const temp = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, temp);
    return temp;
  }

  put(key, value) {
    if(this.cache.has(key)) this.cache.delete(key);
    else if(this.cache.size >= this.maxSize) this.cache.delete(this.cache.keys().next().value);
    this.cache.set(key, value);
  }
}

在 Map 对象中获取缓存数据时,会将数据从 Map 中移出出去,然后再将移出的数据重新放回到 Map 的末尾,这能保证最近使用过的数据总是在 Map 对象的结尾(因 Map 对象的有序性)。

当缓存已满时,就将 Map 开头的数据(也就是最近最少使用的数据)删除,然后再插入新的数据。


4.2 使用 Map 计算字符串中每个字符的出现次数

不知道你有没有遇到过类似的问题,要求统计一个字符串中每个字符出现的次数。针对这个问题,用 Map 即可解决。

function countChars(str) {
  const countMap = new Map();
  for (let char of str) {
    if (countMap.has(char)) {
      countMap.set(char, countMap.get(char) + 1);
    } else {
      countMap.set(char, 1);
    }
  }
  return countMap;
}

console.log(countChars("hello world"));  // Map(8) { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }

上面的代码遍历了输入的字符串,对于其中的每个字符,如果它已经存在于 Map 中,那么就将其数量 +1;如果它还不在 Map 中,那就将其添加到 Map 中,并将其数量设置为 1。

最后的 Map 中,Map 对象的每个键就是字符串中的字符,值就是该字符出现的次数。


5. Map 对象的坑和注意事项

这里要提一下,当我们使用新的工具或技术时,了解其优点和功能固然重要,但是,同样重要的是理解其局限性。稍不注意,你可能就掉坑里了~

对于 JS 中的 Map 对象来说,虽然它提供了不少很强大的功能,但在某些情况下,使用 Map 对象或许不是最好的选择。

另外,Map 对象在处理某些特殊的值的时候,其行为也可能与我们的预期不同。


5.1 什么情况下不应该使用 Map 对象?

首先,数据量小且固定。如果你存储的数据量很小,并且键名是固定的,那么使用普通的 Object 对象会更为简单和高效。因为 Object 的创建和访问速度通常比 Map 要快不少。

再一个,不需要顺序的键。你已经知道,Map 对象会保持键值对的插入顺序,这在某些场景下非常有用。而如果你没有这方面的需求的话,使用 Object 对象反而会更好。

最后是不需要操作复杂数据的情况。Map 对象提供的方法都比较简单,如果你有需要操作复杂数据的需求,比如过滤、映射、排序等,这时候使用 Array 或其他数据结构或许会更方便。


5.2 Map 对象在特殊情况下的行为

在使用 Map 时,还要注意一些特殊情况,下面提到了几种容易让人造成困扰的地方。

NaN 和 NaN:在 Map 对象中,NaN 被视为与自身相等的唯一值。也就是说,你可以将 NaN 作为对象中的一个键,而且 Map 对象会将所有 NaN 视为同一个键。

let map = new Map();
map.set(NaN, 'value');
console.log(map.get(NaN)); // 'value'

+0 和 -0:+0 和 -0 在 Map 对象中被视为同一个键。而在 Object 对象中则不同,在 Object 对象中,+0 和 -0 作为键是不同的。

let map = new Map();
map.set(+0, 'positive');
map.set(-0, 'negative');
console.log(map.get(+0)); // 'negative'
console.log(map.get(-0)); // 'negative'

键的比较:在 JS 中,Map 对象在比较键时使用的是一种叫做"same-value equality”的算法。这个算法的主要特点是:

•  它会认为 NaN 等于 NaN;

•  +0 和 -0 被认为相同。

这与 JS 中的严格相等运算符(===)的行为不同,在严格相等运算符中,NaN 不等于 NaN,+0 和 -0 也是不同的。

let map = new Map();

// 在 Map 对象中,NaN 被视为与自身相等的唯一值
map.set(NaN, 'test');
console.log(map.get(NaN)); // 输出 'test'

// 在 Map 对象中,+0 和 -0 被视为同一个键
map.set(+0, 'positive');
map.set(-0, 'negative');
console.log(map.get(+0)); // 输出 'negative'
console.log(map.get(-0)); // 输出 'negative'

在上面的例子中你可以看到,虽然在 JS中,NaN 不等于 NaN,+0 和 -0 不相同,但是在 Map 对象中,NaN 被视为与自身相等的唯一值,而 +0 和 -0 被视为同一个键。在使用 Map 对象时,这一点是需要格外注意的。


6. 小结

我们已经深入探讨了 JavaScript 中的 Map 对象,包括其遍历方法、进阶用法,以及与 Object 的比较。我们还通过实例分析,展示了 Map 在实际问题中的应用。希望你能从中收获一些有用的知识,也希望这些知识能帮助你在编程中更好地使用 Map 对象。

Javascript

Map用法

Object用法

阅读排行

  • 1. 几行代码就能实现Html大转盘抽奖

    大转盘抽奖是网络互动营销的一种常见形式,其通过简单易懂的界面设计,让用户在游戏中体验到乐趣,同时也能增加商家与用户之间的互动。本文将详细介绍如何使用HTML,CSS和JavaScript来实现大转盘抽奖的功能。

    查看详情
  • 2. 微信支付商户申请接入流程

    微信支付,是微信向有出售物品/提供服务需求的商家提供推广销售、支付收款、经营分析的整套解决方案,包括多种支付方式,如JSAPI支付、小程序支付、APP支付H5支付等支付方式接入。

    查看详情
  • 3. 浙江省同区域公司地址变更详细流程

    提前准备好所有需要的资料,包含:房屋租赁合同、房产证、营业执照正副本、代理人身份证正反面、承诺书(由于我们公司其中一区域已有注册另外一公司,所以必须需要承诺书)

    查看详情
  • 4. 阿里云域名ICP网络备案流程

    根据《互联网信息服务管理办法》以及《非经营性互联网信息服务备案管理办法》,国家对非经营性互联网信息服务实行备案制度,对经营性互联网信息服务实行许可制度。

    查看详情
  • 5. 微信小程序申请注册流程

    微信小程序注册流程与微信公众号较为相似,同时微信小程序支持通过已认证的微信公众号进行注册申请,无需进行单独认证即可使用,同一个已认证微信公众号可同时绑定注册多个小程序。

    查看详情