Javascript Map and Set(映射和集合)

2023-02-17 10:49 更新

學(xué)到現(xiàn)在,我們已經(jīng)了解了以下復(fù)雜的數(shù)據(jù)結(jié)構(gòu):

  • 對(duì)象,存儲(chǔ)帶有鍵的數(shù)據(jù)的集合。
  • 數(shù)組,存儲(chǔ)有序集合。

但這還不足以應(yīng)對(duì)現(xiàn)實(shí)情況。這就是為什么存在 ?Map? 和 ?Set?。

Map

Map 是一個(gè)帶鍵的數(shù)據(jù)項(xiàng)的集合,就像一個(gè) ?Object? 一樣。 但是它們最大的差別是 ?Map? 允許任何類(lèi)型的鍵(key)。

它的方法和屬性如下:

它的方法和屬性如下:

  • ?new Map()? —— 創(chuàng)建 map。
  • ?map.set(key, value)? —— 根據(jù)鍵存儲(chǔ)值。
  • ?map.get(key)? —— 根據(jù)鍵來(lái)返回值,如果 ?map? 中不存在對(duì)應(yīng)的 ?key?,則返回 ?undefined?。
  • ?map.has(key)? —— 如果 ?key? 存在則返回 ?true?,否則返回 ?false?。
  • ?map.delete(key)? —— 刪除指定鍵的值。
  • ?map.clear()? —— 清空 map。
  • ?map.size? —— 返回當(dāng)前元素個(gè)數(shù)。

舉個(gè)例子:

let map = new Map();

map.set('1', 'str1');   // 字符串鍵
map.set(1, 'num1');     // 數(shù)字鍵
map.set(true, 'bool1'); // 布爾值鍵

// 還記得普通的 Object 嗎? 它會(huì)將鍵轉(zhuǎn)化為字符串
// Map 則會(huì)保留鍵的類(lèi)型,所以下面這兩個(gè)結(jié)果不同:
alert( map.get(1)   ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3

如我們所見(jiàn),與對(duì)象不同,鍵不會(huì)被轉(zhuǎn)換成字符串。鍵可以是任何類(lèi)型。

?map[key]? 不是使用 ?Map? 的正確方式

雖然 map[key] 也有效,例如我們可以設(shè)置 map[key] = 2,這樣會(huì)將 map 視為 JavaScript 的 plain object,因此它暗含了所有相應(yīng)的限制(僅支持 string/symbol 鍵等)。

所以我們應(yīng)該使用 map 方法:set 和 get 等。

Map 還可以使用對(duì)象作為鍵。

例如:

let john = { name: "John" };

// 存儲(chǔ)每個(gè)用戶的來(lái)訪次數(shù)
let visitsCountMap = new Map();

// john 是 Map 中的鍵
visitsCountMap.set(john, 123);

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

使用對(duì)象作為鍵是 Map 最值得注意和重要的功能之一。在 Object 中,我們則無(wú)法使用對(duì)象作為鍵。在 Object 中使用字符串作為鍵是可以的,但我們無(wú)法使用另一個(gè) Object 作為 Object 中的鍵。

我們來(lái)嘗試一下:

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

let visitsCountObj = {}; // 嘗試使用對(duì)象

visitsCountObj[ben] = 234; // 嘗試將對(duì)象 ben 用作鍵
visitsCountObj[john] = 123; // 嘗試將對(duì)象 john 用作鍵,但我們會(huì)發(fā)現(xiàn)使用對(duì)象 ben 作為鍵存下的值會(huì)被替換掉

// 變成這樣了!
alert( visitsCountObj["[object Object]"] ); // 123

因?yàn)?nbsp;visitsCountObj 是一個(gè)對(duì)象,它會(huì)將所有 Object 鍵例如上面的 john 和 ben 轉(zhuǎn)換為字符串 "[object Object]"。這顯然不是我們想要的結(jié)果。

?Map? 是怎么比較鍵的?

Map 使用 SameValueZero 算法來(lái)比較鍵是否相等。它和嚴(yán)格等于 === 差不多,但區(qū)別是 NaN 被看成是等于 NaN。所以 NaN 也可以被用作鍵。

這個(gè)算法不能被改變或者自定義。

鏈?zhǔn)秸{(diào)用

每一次 map.set 調(diào)用都會(huì)返回 map 本身,所以我們可以進(jìn)行“鏈?zhǔn)健闭{(diào)用:

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

Map 迭代

如果要在 ?map? 里使用循環(huán),可以使用以下三個(gè)方法:

  • ?map.keys()? —— 遍歷并返回一個(gè)包含所有鍵的可迭代對(duì)象,
  • ?map.values()? —— 遍歷并返回一個(gè)包含所有值的可迭代對(duì)象,
  • ?map.entries()? —— 遍歷并返回一個(gè)包含所有實(shí)體 ?[key, value]? 的可迭代對(duì)象,?for..of? 在默認(rèn)情況下使用的就是這個(gè)。

例如:

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

// 遍歷所有的鍵(vegetables)
for (let vegetable of recipeMap.keys()) {
  alert(vegetable); // cucumber, tomatoes, onion
}

// 遍歷所有的值(amounts)
for (let amount of recipeMap.values()) {
  alert(amount); // 500, 350, 50
}

// 遍歷所有的實(shí)體 [key, value]
for (let entry of recipeMap) { // 與 recipeMap.entries() 相同
  alert(entry); // cucumber,500 (and so on)
}

使用插入順序

迭代的順序與插入值的順序相同。與普通的 Object 不同,Map 保留了此順序。

除此之外,Map 有內(nèi)建的 forEach 方法,與 Array 類(lèi)似:

// 對(duì)每個(gè)鍵值對(duì) (key, value) 運(yùn)行 forEach 函數(shù)
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Object.entries:從對(duì)象創(chuàng)建 Map

當(dāng)創(chuàng)建一個(gè) Map 后,我們可以傳入一個(gè)帶有鍵值對(duì)的數(shù)組(或其它可迭代對(duì)象)來(lái)進(jìn)行初始化,如下所示:

// 鍵值對(duì) [key, value] 數(shù)組
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);

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

如果我們想從一個(gè)已有的普通對(duì)象(plain object)來(lái)創(chuàng)建一個(gè) Map,那么我們可以使用內(nèi)建方法 Object.entries(obj),該方法返回對(duì)象的鍵/值對(duì)數(shù)組,該數(shù)組格式完全按照 Map 所需的格式。

所以可以像下面這樣從一個(gè)對(duì)象創(chuàng)建一個(gè) Map:

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

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

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

這里,Object.entries 返回鍵/值對(duì)數(shù)組:[ ["name","John"], ["age", 30] ]。這就是 Map 所需要的格式。

Object.fromEntries:從 Map 創(chuàng)建對(duì)象

我們剛剛已經(jīng)學(xué)習(xí)了如何使用 Object.entries(obj) 從普通對(duì)象(plain object)創(chuàng)建 Map。

Object.fromEntries 方法的作用是相反的:給定一個(gè)具有 [key, value] 鍵值對(duì)的數(shù)組,它會(huì)根據(jù)給定數(shù)組創(chuàng)建一個(gè)對(duì)象:

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

// 現(xiàn)在 prices = { banana: 1, orange: 2, meat: 4 }

alert(prices.orange); // 2

我們可以使用 Object.fromEntries 從 Map 得到一個(gè)普通對(duì)象(plain object)。

例如,我們?cè)?nbsp;Map 中存儲(chǔ)了一些數(shù)據(jù),但是我們需要把這些數(shù)據(jù)傳給需要普通對(duì)象(plain object)的第三方代碼。

我們來(lái)開(kāi)始:

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

let obj = Object.fromEntries(map.entries()); // 創(chuàng)建一個(gè)普通對(duì)象(plain object)(*)

// 完成了!
// obj = { banana: 1, orange: 2, meat: 4 }

alert(obj.orange); // 2

調(diào)用 map.entries() 將返回一個(gè)可迭代的鍵/值對(duì),這剛好是 Object.fromEntries 所需要的格式。

我們可以把帶 (*) 這一行寫(xiě)得更短:

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

上面的代碼作用也是一樣的,因?yàn)?nbsp;Object.fromEntries 期望得到一個(gè)可迭代對(duì)象作為參數(shù),而不一定是數(shù)組。并且 map 的標(biāo)準(zhǔn)迭代會(huì)返回跟 map.entries() 一樣的鍵/值對(duì)。因此,我們可以獲得一個(gè)普通對(duì)象(plain object),其鍵/值對(duì)與 map 相同。

Set

Set 是一個(gè)特殊的類(lèi)型集合 —— “值的集合”(沒(méi)有鍵),它的每一個(gè)值只能出現(xiàn)一次。

它的主要方法如下:

  • ?new Set(iterable)? —— 創(chuàng)建一個(gè) ?set?,如果提供了一個(gè) ?iterable? 對(duì)象(通常是數(shù)組),將會(huì)從數(shù)組里面復(fù)制值到 ?set? 中。
  • ?set.add(value)? —— 添加一個(gè)值,返回 set 本身
  • ?set.delete(value)? —— 刪除值,如果 ?value? 在這個(gè)方法調(diào)用的時(shí)候存在則返回 ?true? ,否則返回 ?false?。
  • ?set.has(value)? —— 如果 ?value? 在 set 中,返回 ?true?,否則返回 ?false?。
  • ?set.clear()? —— 清空 set。
  • ?set.size? —— 返回元素個(gè)數(shù)。

它的主要特點(diǎn)是,重復(fù)使用同一個(gè)值調(diào)用 set.add(value) 并不會(huì)發(fā)生什么改變。這就是 Set 里面的每一個(gè)值只出現(xiàn)一次的原因。

例如,我們有客人來(lái)訪,我們想記住他們每一個(gè)人。但是已經(jīng)來(lái)訪過(guò)的客人再次來(lái)訪,不應(yīng)造成重復(fù)記錄。每個(gè)訪客必須只被“計(jì)數(shù)”一次。

Set 可以幫助我們解決這個(gè)問(wèn)題:

let set = new Set();

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

// visits,一些訪客來(lái)訪好幾次
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set 只保留不重復(fù)的值
alert( set.size ); // 3

for (let user of set) {
  alert(user.name); // John(然后 Pete 和 Mary)
}

Set 的替代方法可以是一個(gè)用戶數(shù)組,用 arr.find 在每次插入值時(shí)檢查是否重復(fù)。但是這樣性能會(huì)很差,因?yàn)檫@個(gè)方法會(huì)遍歷整個(gè)數(shù)組來(lái)檢查每個(gè)元素。Set 內(nèi)部對(duì)唯一性檢查進(jìn)行了更好的優(yōu)化。

Set 迭代(iteration)

我們可以使用 for..of 或 forEach 來(lái)遍歷 Set:

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

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

// 與 forEach 相同:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

注意一件有趣的事兒。forEach 的回調(diào)函數(shù)有三個(gè)參數(shù):一個(gè) value,然后是 同一個(gè)值 valueAgain,最后是目標(biāo)對(duì)象。沒(méi)錯(cuò),同一個(gè)值在參數(shù)里出現(xiàn)了兩次。

forEach 的回調(diào)函數(shù)有三個(gè)參數(shù),是為了與 Map 兼容。當(dāng)然,這看起來(lái)確實(shí)有些奇怪。但是這對(duì)在特定情況下輕松地用 Set 代替 Map 很有幫助,反之亦然。

Map 中用于迭代的方法在 Set 中也同樣支持:

  • ?set.keys()? —— 遍歷并返回一個(gè)包含所有值的可迭代對(duì)象,
  • ?set.values()? —— 與 ?set.keys()? 作用相同,這是為了兼容 ?Map?,
  • ?set.entries()? —— 遍歷并返回一個(gè)包含所有的實(shí)體 ?[value, value]? 的可迭代對(duì)象,它的存在也是為了兼容 ?Map?。

總結(jié)

Map —— 是一個(gè)帶鍵的數(shù)據(jù)項(xiàng)的集合。

方法和屬性如下:

  • ?new Map([iterable])? —— 創(chuàng)建 map,可選擇帶有 ?[key,value]? 對(duì)的 ?iterable?(例如數(shù)組)來(lái)進(jìn)行初始化。
  • ?map.set(key, value)? —— 根據(jù)鍵存儲(chǔ)值,返回 map 自身。
  • ?map.get(key)? —— 根據(jù)鍵來(lái)返回值,如果 ?map? 中不存在對(duì)應(yīng)的 ?key?,則返回 ?undefined?。
  • ?map.has(key)? —— 如果 ?key? 存在則返回 ?true?,否則返回 ?false?。
  • ?map.delete(key)? —— 刪除指定鍵對(duì)應(yīng)的值,如果在調(diào)用時(shí) ?key? 存在,則返回 ?true?,否則返回 ?false?。
  • ?map.clear()? —— 清空 map 。
  • ?map.size? —— 返回當(dāng)前元素個(gè)數(shù)。

與普通對(duì)象 Object 的不同點(diǎn):

  • 任何鍵、對(duì)象都可以作為鍵。
  • 有其他的便捷方法,如 ?size? 屬性。

Set —— 是一組唯一值的集合。

方法和屬性:

  • ?new Set([iterable])? —— 創(chuàng)建 set,可選擇帶有 ?iterable?(例如數(shù)組)來(lái)進(jìn)行初始化。
  • ?set.add(value)? —— 添加一個(gè)值(如果 ?value? 存在則不做任何修改),返回 set 本身。
  • ?set.delete(value)? —— 刪除值,如果 ?value? 在這個(gè)方法調(diào)用的時(shí)候存在則返回 ?true? ,否則返回 ?false?。
  • ?set.has(value)? —— 如果 ?value? 在 set 中,返回 ?true?,否則返回 ?false?。
  • ?set.clear()? —— 清空 set。
  • ?set.size? —— 元素的個(gè)數(shù)。

在 Map 和 Set 中迭代總是按照值插入的順序進(jìn)行的,所以我們不能說(shuō)這些集合是無(wú)序的,但是我們不能對(duì)元素進(jìn)行重新排序,也不能直接按其編號(hào)來(lái)獲取元素。

任務(wù)


過(guò)濾數(shù)組中的唯一元素

重要程度: 5

定義 ?arr? 為一個(gè)數(shù)組。

創(chuàng)建一個(gè)函數(shù) ?unique(arr)?,該函數(shù)返回一個(gè)由 ?arr? 中所有唯一元素所組成的數(shù)組。

例如:

function unique(arr) {
  /* 你的代碼 */
}

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

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

P.S. 這里用到了 string 類(lèi)型,但其實(shí)可以是任何類(lèi)型的值。

P.S. 使用 ?Set? 來(lái)存儲(chǔ)唯一值。


解決方案

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

過(guò)濾字謎(anagrams)

重要程度: 4

Anagrams 是具有相同數(shù)量相同字母但是順序不同的單詞。

例如:

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

寫(xiě)一個(gè)函數(shù) aclean(arr),它返回被清除了字謎(anagrams)的數(shù)組。

例如:

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

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

對(duì)于所有的字謎(anagram)組,都應(yīng)該保留其中一個(gè)詞,但保留的具體是哪一個(gè)并不重要。


解決方案

為了找到所有字謎(anagram),讓我們把每個(gè)單詞打散為字母并進(jìn)行排序。當(dāng)字母被排序后,所有的字謎就都一樣了。

例如:

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

我們將使用進(jìn)行字母排序后的單詞的變體(variant)作為 map 的鍵,每個(gè)鍵僅對(duì)應(yīng)存儲(chǔ)一個(gè)值:

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

  for (let word of arr) {
    // 將單詞 split 成字母,對(duì)字母進(jìn)行排序,之后再 join 回來(lái)
    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) );

字母排序在 (*) 行以鏈?zhǔn)秸{(diào)用的方式完成。

為了方便,我們把它分解為多行:

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

兩個(gè)不同的單詞 'PAN' 和 'nap' 得到了同樣的字母排序形式 'anp'。

下一行是將單詞放入 map:

map.set(sorted, word);

如果我們?cè)俅斡龅较嗤帜概判蛐问降膯卧~,那么它將會(huì)覆蓋 map 中有相同鍵的前一個(gè)值。因此,每個(gè)字母形式(譯注:排序后的)最多只有一個(gè)單詞。(譯注:并且是每個(gè)字母形式中最靠后的那個(gè)值)

最后,Array.from(map.values()) 將 map 的值迭代(我們不需要結(jié)果的鍵)為數(shù)組形式,并返回這個(gè)數(shù)組。

在這里,我們也可以使用普通對(duì)象(plain object)而不用 Map,因?yàn)殒I就是字符串。

下面是解決方案:

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()? 得到一個(gè)數(shù)組,然后使用例如 ?.push? 等特定的方法對(duì)其進(jìn)行處理。

但是運(yùn)行不了:

let map = new Map();

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

let keys = map.keys();

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

為什么?我們應(yīng)該如何修改代碼讓 keys.push 工作?


解決方案

這是因?yàn)?nbsp;map.keys() 返回的是可迭代對(duì)象而非數(shù)組。

我們可以使用方法 Array.from 來(lái)將它轉(zhuǎn)換為數(shù)組:

let map = new Map();

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

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

keys.push("more");

alert(keys); // name, more


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)