2022 年 11 月 14 日

Map 和 Set

到目前为止,我们已经学习了以下复杂的数据结构

  • 对象用于存储键控集合。
  • 数组用于存储有序集合。

但这对于实际生活来说还不够。这就是 MapSet 也存在的原因。

Map

Map 是一个键控数据项集合,就像一个 Object。但主要区别在于 Map 允许任何类型的键。

方法和属性是

例如

let map = new Map();

map.set('1', 'str1');   // a string key
map.set(1, 'num1');     // a numeric key
map.set(true, 'bool1'); // a boolean key

// remember the regular Object? it would convert keys to string
// Map keeps the type, so these two are different:
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

正如我们所见,与对象不同,键不会转换为字符串。任何类型的键都是可能的。

map[key] 不是使用 Map 的正确方法

虽然 map[key] 也可行,例如我们可以设置 map[key] = 2,但这将 map 视为一个普通的 JavaScript 对象,因此它暗示了所有相应的限制(仅字符串/符号键等)。

所以我们应该使用 map 方法:setget 等。

Map 还可以使用对象作为键。

例如

let john = { name: "John" };

// for every user, let's store their visits count
let visitsCountMap = new Map();

// john is the key for the map
visitsCountMap.set(john, 123);

alert( visitsCountMap.get(john) ); // 123

使用对象作为键是 Map 最显着和最重要的特性之一。Object 则不同。字符串作为 Object 中的键是可以的,但我们不能在 Object 中使用另一个 Object 作为键。

我们尝试一下

let john = { name: "John" };
let ben = { name: "Ben" };

let visitsCountObj = {}; // try to use an object

visitsCountObj[ben] = 234; // try to use ben object as the key
visitsCountObj[john] = 123; // try to use john object as the key, ben object will get replaced

// That's what got written!
alert( visitsCountObj["[object Object]"] ); // 123

由于 visitsCountObj 是一个对象,因此它会将所有 Object 键(如上面的 johnben)转换为相同的字符串 "[object Object]"。这绝对不是我们想要的。

Map 如何比较键

为了测试键的等价性,Map 使用算法 SameValueZero。它与严格相等 === 大致相同,但不同之处在于 NaN 被认为等于 NaN。因此 NaN 也可以用作键。

此算法不能更改或自定义。

链式

每个 map.set 调用都会返回 map 本身,因此我们可以“链接”调用

map.set('1', 'str1')
  .set(1, 'num1')
  .set(true, 'bool1');

遍历 Map

对于循环遍历一个 map,有 3 种方法

  • map.keys() – 返回一个可迭代的键
  • map.values() – 返回一个可迭代的值
  • map.entries() – 返回一个可迭代的条目 [key, value],它在 for..of 中默认使用。

例如

let recipeMap = new Map([
  ['cucumber', 500],
  ['tomatoes', 350],
  ['onion',    50]
]);

// iterate over keys (vegetables)
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// iterate over values (amounts)
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// iterate over [key, value] entries
for (let entry of recipeMap) { // the same as of recipeMap.entries()
  alert(entry); // cucumber,500 (and so on)
}
使用插入顺序

迭代的顺序与插入值时的顺序相同。Map 保留此顺序,这与常规 Object 不同。

除此之外,Map 还具有一个内置的 forEach 方法,类似于 Array

// runs the function for each (key, value) pair
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Object.entries:从 Object 映射

创建 Map 时,我们可以传递一个包含键/值对的数组(或其他可迭代对象)进行初始化,如下所示

// array of [key, value] pairs
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

alert( map.get('1') ); // str1

如果我们有一个普通对象,并且我们希望从中创建一个 Map,那么我们可以使用内置方法 Object.entries(obj),它返回一个键/值对数组,该数组的格式完全符合对象。

因此,我们可以像这样从对象创建一个映射

let obj = {
  name: "John",
  age: 30
};

let map = new Map(Object.entries(obj));

alert( map.get('name') ); // John

此处,Object.entries 返回键/值对数组:[ ["name","John"], ["age", 30] ]。这就是 Map 所需的。

Object.fromEntries:从 Map 映射

我们刚刚看到如何使用 Object.entries(obj) 从普通对象创建 Map

有一个 Object.fromEntries 方法可以执行相反的操作:给定一个 [key, value] 对数组,它会从中创建一个对象

let prices = Object.fromEntries([
  ['banana', 1],
  ['orange', 2],
  ['meat', 4]
]);

// now prices = { banana: 1, orange: 2, meat: 4 }

alert(prices.orange); // 2

我们可以使用 Object.fromEntriesMap 获取一个普通对象。

例如,我们将数据存储在 Map 中,但我们需要将其传递给期望普通对象的第三方代码。

我们开始

let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);

let obj = Object.fromEntries(map.entries()); // make a plain object (*)

// done!
// obj = { banana: 1, orange: 2, meat: 4 }

alert(obj.orange); // 2

调用 map.entries() 返回一个键/值对的可迭代对象,其格式完全符合 Object.fromEntries

我们还可以缩短行 (*)

let obj = Object.fromEntries(map); // omit .entries()

这是相同的,因为 Object.fromEntries 期望一个可迭代对象作为参数。不一定是数组。并且 map 的标准迭代返回与 map.entries() 相同的键/值对。因此,我们得到一个与 map 具有相同键/值的普通对象。

Set

Set 是一种特殊的类型集合——“值集合”(没有键),其中每个值只能出现一次。

它的主要方法是

  • new Set([iterable]) – 创建集合,如果提供了 iterable 对象(通常是一个数组),则将值从中复制到集合中。
  • set.add(value) – 添加一个值,返回集合本身。
  • set.delete(value) – 删除该值,如果 value 在调用时存在,则返回 true,否则返回 false
  • set.has(value) – 如果值存在于集合中,则返回 true,否则返回 false
  • set.clear() – 从集合中移除所有内容。
  • set.size – 是元素计数。

主要特点是重复调用 set.add(value),其中值相同,不会执行任何操作。这就是每个值仅在 Set 中出现一次的原因。

例如,我们有访客来访,我们希望记住每个人。但重复访问不应导致重复项。访客必须仅被“计数”一次。

Set 正好适合这种情况

let set = new Set();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// visits, some users come multiple times
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set keeps only unique values
alert( set.size ); // 3

for (let user of set) {
  alert(user.name); // John (then Pete and Mary)
}

Set 的替代方案可能是用户数组,以及使用 arr.find 在每次插入时检查重复项的代码。但性能会差很多,因为此方法会遍历整个数组并检查每个元素。Set 在内部针对唯一性检查进行了更好的优化。

遍历 Set

我们可以使用 for..offorEach 循环遍历集合

let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) alert(value);

// the same with forEach:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

请注意有趣的事情。在 forEach 中传递的回调函数有 3 个参数:一个 value,然后是相同的值 valueAgain,然后是目标对象。事实上,相同的值在参数中出现两次。

这是为了与 Map 兼容,其中传递给 forEach 的回调函数有三个参数。看起来有点奇怪,这是肯定的。但在某些情况下,这可能有助于轻松地用 Set 替换 Map,反之亦然。

Map 为迭代器提供的相同方法也受支持

  • set.keys() – 返回值的迭代对象,
  • set.values() – 与 set.keys() 相同,为了与 Map 兼容,
  • set.entries() – 返回条目 [value, value] 的迭代对象,存在是为了与 Map 兼容。

总结

Map – 是键控值集合。

方法和属性

  • new Map([iterable]) – 创建映射,其中包含用于初始化的 [key,value] 对的可选 iterable(例如数组)。
  • map.set(key, value) – 按键存储值,返回映射本身。
  • map.get(key) – 通过键返回该值,如果 key 不存在于 map 中,则返回 undefined
  • map.has(key) – 如果 key 存在,则返回 true,否则返回 false
  • map.delete(key) – 按键删除元素,如果调用时key存在,则返回true,否则返回false
  • map.clear() – 从 map 中移除所有内容。
  • map.size – 返回当前元素数量。

与普通Object的区别

  • 任何键,对象可以是键。
  • 其他便捷方法,size属性。

Set – 是一个唯一值集合。

方法和属性

  • new Set([iterable]) – 创建集合,带有可选的iterable(例如数组)值,用于初始化。
  • set.add(value) – 添加一个值(如果value存在,则不执行任何操作),返回集合本身。
  • set.delete(value) – 删除该值,如果 value 在调用时存在,则返回 true,否则返回 false
  • set.has(value) – 如果值存在于集合中,则返回 true,否则返回 false
  • set.clear() – 从集合中移除所有内容。
  • set.size – 是元素计数。

MapSet的迭代始终按插入顺序进行,因此我们不能说这些集合是无序的,但我们不能重新排列元素或直接按其编号获取元素。

任务

重要性:5

arr为一个数组。

创建一个函数unique(arr),该函数应返回一个包含arr唯一项的数组。

例如

function unique(arr) {
  /* your code */
}

let values = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(values) ); // Hare, Krishna, :-O

P.S.此处使用字符串,但可以是任何类型的值。

P.P.S.使用Set存储唯一值。

打开一个带有测试的沙箱。

function unique(arr) {
  return Array.from(new Set(arr));
}

在沙箱中打开带有测试的解决方案。

重要性:4

同形异位词是指具有相同数量的相同字母,但顺序不同的单词。

例如

nap - pan
ear - are - era
cheaters - hectares - teachers

编写一个函数aclean(arr),该函数返回一个已从同形异位词中清理的数组。

例如

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) ); // "nap,teachers,ear" or "PAN,cheaters,era"

从每个同形异位词组中,只保留一个单词,无论哪个单词。

打开一个带有测试的沙箱。

要查找所有同形异位词,让我们将每个单词拆分为字母并对其进行排序。按字母排序后,所有同形异位词都是相同的。

例如

nap, pan -> anp
ear, era, are -> aer
cheaters, hectares, teachers -> aceehrst
...

我们将使用按字母排序的变体作为映射键,以便每个键只存储一个值

function aclean(arr) {
  let map = new Map();

  for (let word of arr) {
    // split the word by letters, sort them and join back
    let sorted = word.toLowerCase().split('').sort().join(''); // (*)
    map.set(sorted, word);
  }

  return Array.from(map.values());
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

字母排序由 (*) 行中的调用链完成。

为了方便,我们将其拆分为多行

let sorted = word // PAN
  .toLowerCase() // pan
  .split('') // ['p','a','n']
  .sort() // ['a','n','p']
  .join(''); // anp

两个不同的单词 'PAN''nap' 接收相同的字母排序形式 'anp'

下一行将单词放入映射中

map.set(sorted, word);

如果我们再次遇到具有相同字母排序形式的单词,那么它将用具有映射中相同键的先前值覆盖。因此,对于每个字母形式,我们始终最多只有一个单词。

最后,Array.from(map.values()) 采用映射值的可迭代对象(我们不需要结果中的键),并返回它们的数组。

在这里,我们还可以使用普通对象而不是 Map,因为键是字符串。

解决方案可以这样

function aclean(arr) {
  let obj = {};

  for (let i = 0; i < arr.length; i++) {
    let sorted = arr[i].toLowerCase().split("").sort().join("");
    obj[sorted] = arr[i];
  }

  return Object.values(obj);
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

在沙箱中打开带有测试的解决方案。

重要性:5

我们希望在变量中获取 map.keys() 的数组,然后对其应用特定于数组的方法,例如 .push

但这不起作用

let map = new Map();

map.set("name", "John");

let keys = map.keys();

// Error: keys.push is not a function
keys.push("more");

为什么?我们如何修复代码以使 keys.push 起作用?

这是因为 map.keys() 返回一个可迭代对象,而不是一个数组。

我们可以使用 Array.from 将其转换为数组

let map = new Map();

map.set("name", "John");

let keys = Array.from(map.keys());

keys.push("more");

alert(keys); // name, more
教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议,请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行,请将其包装在 <pre> 标记中,对于 10 行以上,请使用沙盒 (plnkrjsbincodepen…)