Javascript Map and Set(映射和集合)

2023-02-17 10:49 更新

學到現(xiàn)在,我們已經了解了以下復雜的數(shù)據結構:

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

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

Map

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

它的方法和屬性如下:

它的方法和屬性如下:

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

舉個例子:

let map = new Map();

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

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

alert( map.size ); // 3

如我們所見,與對象不同,鍵不會被轉換成字符串。鍵可以是任何類型。

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

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

所以我們應該使用 map 方法:set 和 get 等。

Map 還可以使用對象作為鍵。

例如:

let john = { name: "John" };

// 存儲每個用戶的來訪次數(shù)
let visitsCountMap = new Map();

// john 是 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 = {}; // 嘗試使用對象

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

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

因為 visitsCountObj 是一個對象,它會將所有 Object 鍵例如上面的 john 和 ben 轉換為字符串 "[object Object]"。這顯然不是我們想要的結果。

?Map? 是怎么比較鍵的?

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

這個算法不能被改變或者自定義。

鏈式調用

每一次 map.set 調用都會返回 map 本身,所以我們可以進行“鏈式”調用:

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

Map 迭代

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

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

例如:

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
}

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

使用插入順序

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

除此之外,Map 有內建的 forEach 方法,與 Array 類似:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 得到一個普通對象(plain object)。

例如,我們在 Map 中存儲了一些數(shù)據,但是我們需要把這些數(shù)據傳給需要普通對象(plain object)的第三方代碼。

我們來開始:

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

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

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

alert(obj.orange); // 2

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

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

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

上面的代碼作用也是一樣的,因為 Object.fromEntries 期望得到一個可迭代對象作為參數(shù),而不一定是數(shù)組。并且 map 的標準迭代會返回跟 map.entries() 一樣的鍵/值對。因此,我們可以獲得一個普通對象(plain object),其鍵/值對與 map 相同。

Set

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

它的主要方法如下:

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

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

例如,我們有客人來訪,我們想記住他們每一個人。但是已經來訪過的客人再次來訪,不應造成重復記錄。每個訪客必須只被“計數(shù)”一次。

Set 可以幫助我們解決這個問題:

let set = new Set();

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

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

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

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

Set 的替代方法可以是一個用戶數(shù)組,用 arr.find 在每次插入值時檢查是否重復。但是這樣性能會很差,因為這個方法會遍歷整個數(shù)組來檢查每個元素。Set 內部對唯一性檢查進行了更好的優(yōu)化。

Set 迭代(iteration)

我們可以使用 for..of 或 forEach 來遍歷 Set:

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

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

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

注意一件有趣的事兒。forEach 的回調函數(shù)有三個參數(shù):一個 value,然后是 同一個值 valueAgain,最后是目標對象。沒錯,同一個值在參數(shù)里出現(xiàn)了兩次。

forEach 的回調函數(shù)有三個參數(shù),是為了與 Map 兼容。當然,這看起來確實有些奇怪。但是這對在特定情況下輕松地用 Set 代替 Map 很有幫助,反之亦然。

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

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

總結

Map —— 是一個帶鍵的數(shù)據項的集合。

方法和屬性如下:

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

與普通對象 Object 的不同點:

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

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

方法和屬性:

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

在 Map 和 Set 中迭代總是按照值插入的順序進行的,所以我們不能說這些集合是無序的,但是我們不能對元素進行重新排序,也不能直接按其編號來獲取元素。

任務


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

重要程度: 5

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

創(chuàng)建一個函數(shù) ?unique(arr)?,該函數(shù)返回一個由 ?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 類型,但其實可以是任何類型的值。

P.S. 使用 ?Set? 來存儲唯一值。


解決方案

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

過濾字謎(anagrams)

重要程度: 4

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

例如:

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

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

例如:

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

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

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


解決方案

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

例如:

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

我們將使用進行字母排序后的單詞的變體(variant)作為 map 的鍵,每個鍵僅對應存儲一個值:

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

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

map.set(sorted, word);

如果我們再次遇到相同字母排序形式的單詞,那么它將會覆蓋 map 中有相同鍵的前一個值。因此,每個字母形式(譯注:排序后的)最多只有一個單詞。(譯注:并且是每個字母形式中最靠后的那個值)

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

在這里,我們也可以使用普通對象(plain object)而不用 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()? 得到一個數(shù)組,然后使用例如 ?.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() 返回的是可迭代對象而非數(shù)組。

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

let map = new Map();

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

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

keys.push("more");

alert(keys); // name, more


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號