學到現在,我們已經了解了以下複雜的數據結構:
對象,存儲帶有鍵的數據的集合。
數組,存儲有序集合。
但這還不足以應對現實情況。這就是爲什麽存在 Map
和 Set
。
Map 是壹個帶鍵的數據項的集合,就像壹個 Object
壹樣。 但是它們最大的差別是 Map
允許任何類型的鍵(key)。
它的方法和屬性如下:
new Map()
—— 創建 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
—— 返回當前元素個數。
舉個例子:
let map = new Map(); map.set('1', 'str1'); // 字符串鍵 map.set(1, 'num1'); // 數字鍵 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" }; // 存儲每個用戶的來訪次數 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 用作鍵,但我們會發現使用對象 ben 作爲鍵存下的值會被替換掉 // 變成這樣了! alert( visitsCountObj["[object Object]"] ); // 123
因爲 visitsCountObj
是壹個對象,它會將所有 Object
鍵例如上面的 john
和 ben
轉換爲字符串 "[object Object]"
。這顯然不是我們想要的結果。
Map
是怎麽比較鍵的?
Map
使用 SameValueZero 算法來比較鍵是否相等。它和嚴格等于 ===
差不多,但區別是 NaN
被看成是等于 NaN
。所以 NaN
也可以被用作鍵。
這個算法不能被改變或者自定義。
鏈式調用
每壹次 map.set
調用都會返回 map 本身,所以我們可以進行“鏈式”調用:
map.set('1', 'str1') .set(1, 'num1') .set(true, 'bool1');
如果要在 map
裏使用循環,可以使用以下三個方法:
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 函數 recipeMap.forEach( (value, key, map) => { alert(`${key}: ${value}`); // cucumber: 500 etc });
當創建壹個 Map
後,我們可以傳入壹個帶有鍵值對的數組(或其它可叠代對象)來進行初始化,如下所示:
// 鍵值對 [key, value] 數組 let map = new Map([ ['1', 'str1'], [1, 'num1'], [true, 'bool1'] ]); alert( map.get('1') ); // str1
如果我們想從壹個已有的普通對象(plain object)來創建壹個 Map
,那麽我們可以使用內建方法 Object.entries(obj),該方法返回對象的鍵/值對數組,該數組格式完全按照 Map
所需的格式。
所以可以像下面這樣從壹個對象創建壹個 Map:
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.entries(obj)
從普通對象(plain object)創建 Map
。
Object.fromEntries
方法的作用是相反的:給定壹個具有 [key, value]
鍵值對的數組,它會根據給定數組創建壹個對象:
let prices = Object.fromEntries([ ['banana', 1], ['orange', 2], ['meat', 4] ]); // 現在 prices = { banana: 1, orange: 2, meat: 4 } alert(prices.orange); // 2
我們可以使用 Object.fromEntries
從 Map
得到壹個普通對象(plain object)。
例如,我們在 Map
中存儲了壹些數據,但是我們需要把這些數據傳給需要普通對象(plain object)的第三方代碼。
我們來開始:
let map = new Map(); map.set('banana', 1); map.set('orange', 2); map.set('meat', 4); let obj = Object.fromEntries(map.entries()); // 創建壹個普通對象(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
期望得到壹個可叠代對象作爲參數,而不壹定是數組。並且 map
的標准叠代會返回跟 map.entries()
壹樣的鍵/值對。因此,我們可以獲得壹個普通對象(plain object),其鍵/值對與 map
相同。
Set
是壹個特殊的類型集合 —— “值的集合”(沒有鍵),它的每壹個值只能出現壹次。
它的主要方法如下:
new Set(iterable)
—— 創建壹個 set
,如果提供了壹個 iterable
對象(通常是數組),將會從數組裏面複制值到 set
中。
set.add(value)
—— 添加壹個值,返回 set 本身
set.delete(value)
—— 刪除值,如果 value
在這個方法調用的時候存在則返回 true
,否則返回 false
。
set.has(value)
—— 如果 value
在 set 中,返回 true
,否則返回 false
。
set.clear()
—— 清空 set。
set.size
—— 返回元素個數。
它的主要特點是,重複使用同壹個值調用 set.add(value)
並不會發生什麽改變。這就是 Set
裏面的每壹個值只出現壹次的原因。
例如,我們有客人來訪,我們想記住他們每壹個人。但是已經來訪過的客人再次來訪,不應造成重複記錄。每個訪客必須只被“計數”壹次。
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
的替代方法可以是壹個用戶數組,用 arr.find 在每次插入值時檢查是否重複。但是這樣性能會很差,因爲這個方法會遍曆整個數組來檢查每個元素。Set
內部對唯壹性檢查進行了更好的優化。
我們可以使用 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
的回調函數有三個參數:壹個 value
,然後是 同壹個值 valueAgain
,最後是目標對象。沒錯,同壹個值在參數裏出現了兩次。
forEach
的回調函數有三個參數,是爲了與 Map
兼容。當然,這看起來確實有些奇怪。但是這對在特定情況下輕松地用 Set
代替 Map
很有幫助,反之亦然。
Map
中用于叠代的方法在 Set
中也同樣支持:
set.keys()
—— 遍曆並返回壹個包含所有值的可叠代對象,
set.values()
—— 與 set.keys()
作用相同,這是爲了兼容 Map
,
set.entries()
—— 遍曆並返回壹個包含所有的實體 [value, value]
的可叠代對象,它的存在也是爲了兼容 Map
。
Map
—— 是壹個帶鍵的數據項的集合。
方法和屬性如下:
new Map([iterable])
—— 創建 map,可選擇帶有 [key,value]
對的 iterable
(例如數組)來進行初始化。
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
—— 返回當前元素個數。
與普通對象 Object
的不同點:
任何鍵、對象都可以作爲鍵。
有其他的便捷方法,如 size
屬性。
Set
—— 是壹組唯壹值的集合。
方法和屬性:
new Set([iterable])
—— 創建 set,可選擇帶有 iterable
(例如數組)來進行初始化。
set.add(value)
—— 添加壹個值(如果 value
存在則不做任何修改),返回 set 本身。
set.delete(value)
—— 刪除值,如果 value
在這個方法調用的時候存在則返回 true
,否則返回 false
。
set.has(value)
—— 如果 value
在 set 中,返回 true
,否則返回 false
。
set.clear()
—— 清空 set。
set.size
—— 元素的個數。
在 Map
和 Set
中叠代總是按照值插入的順序進行的,所以我們不能說這些集合是無序的,但是我們不能對元素進行重新排序,也不能直接按其編號來獲取元素。
重要程度: 5
定義 arr
爲壹個數組。
創建壹個函數 unique(arr)
,該函數返回壹個由 arr
中所有唯壹元素所組成的數組。
例如:
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)); }
使用沙箱的測試功能打開解決方案。
重要程度: 4
Anagrams 是具有相同數量相同字母但是順序不同的單詞。
例如:
nap - pan ear - are - era cheaters - hectares - teachers
寫壹個函數 aclean(arr)
,它返回被清除了字謎(anagrams)的數組。
例如:
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 的值叠代(我們不需要結果的鍵)爲數組形式,並返回這個數組。
在這裏,我們也可以使用普通對象(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()
得到壹個數組,然後使用例如 .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