「 Map最佳實踐」什么時候適合使用 Map 而不是 Object

首先我們先有請「Map」簡單介紹下自己
「Map」映射是一種經(jīng)典的數(shù)據(jù)結(jié)構(gòu)類型,其中數(shù)據(jù)以 「key/value」 的鍵值對形式存在
| Map | Object | |
|---|---|---|
| 默認(rèn)值 | 默認(rèn)不包含任何值,只包含顯式插入的鍵 | 一個 Object 有一個原型,原型上的鍵名有可能和自己對象上設(shè)置的鍵名沖突 |
| 類型 | 任意 | String 或 Symbol |
| 長度 | 鍵值對個數(shù)通過 size 屬性獲取 | 鍵值對個數(shù)只能手動計算 |
| 性能 | 頻繁增刪鍵值對的場景下表現(xiàn)更好 | 頻繁添加和刪除鍵值對的場景下未作出優(yōu)化 |
Map 基本用法
接受任何類型的鍵 劃重點,是任何 any!!!

const?testMap?=?new?Map()
let?str?=?'今天不學(xué)習(xí)',
????num?=?666,
????keyFunction?=?function?()?{},
????keySymbol?=?Symbol('Web'),
????keyNull?=?null,
????keyUndefined?=?undefined,
????keyNaN?=?NaN
//添加鍵值對
//基本用法
testMap.set('key',?'value')?//?Map(1)?{"key"?=>?"value"}
testMap.set(str,?'明天變辣雞')
testMap.set(num,?'前端Sneaker')
testMap.set(keyFunction,?'你的函數(shù)寫的好棒棒哦')
testMap.set(keySymbol,?'大前端')
testMap.set(keyNull,?'我是個Null')
testMap.set(keyUndefined,?'我是個Undifined')
testMap.set(keyNaN,?'我是個NaN')
testMap.get(function?()?{})?//undefined
testMap.get(Symbol('Web'))?//undefined
//雖然NaN?!==?NaN?但是作為Map鍵名并無區(qū)別
testMap.get(NaN)?//"我是個NaN"
testMap.get(Number('NaN'))?//"我是個NaN"
除了NaN比較特殊外,其他「Map」的get方法都是通過對比鍵名是否相等(===)來獲取,不相等則返回undefined
比較 Map 和 Object
定義
//Map
const?map?=?new?Map();
map.set('key',?'value');?//?Map(1)?{"key"?=>?"value"}
map.get('key');?//?'value'
//Object
const?someObject?=?{};
someObject.key?=?'value';
someObject.key;?//?'value'
這里可以明顯看出其實其定義行為是十分相似的,想必看到這里大家還沒看出來「Map」到底在何時使用才是最佳實踐,別急接著來。
鍵名類型
JavaScript 「Object」只接收兩種類型的鍵名 String 和 Symbol,你可以使用其他類型的鍵名,但是最終 JavaScript 都會隱式轉(zhuǎn)換為字符串
const?obj?=?{}
//直接看幾種比較特殊的鍵名
obj[true]?=?'Boolean'
obj[1]?=?'Number'
obj[{'前端':'Sneaker'}]?=?'666'
Object.keys(obj)?//?["1",?"true",?"[object?Object]"]
再來看看 「Map」 的,其接收任何類型的鍵名并保留其鍵名類型 (此處簡單舉例,詳細(xì)可看文章開頭「Map」基本使用)
const?map?=?new?Map();
map.set(1,?'value');
map.set(true,?'value');
map.set({'key':?'value'},?'value');
for?(const?key?of?map.keys())?{
??console.log(key);
}
//?1
//?true
//?{key:?"value"}
//除此之外,Map還支持正則作為鍵名
map.set(/^1[3456789]\d{9}$/,'手機(jī)號正則')
//Map(1)?{/^1[3456789]\d{9}$/?=>?"手機(jī)號正則"}
「Map」支持正則表達(dá)式作為鍵名,這在Object是不被允許的直接報錯
原型 Prototype
「Object」不同于「Map」,它不僅僅是表面所看到的。「Map」只包含你所定義的鍵值對,但是「Object」對象具有其原型中的一些內(nèi)置屬性
const?newObject?=?{};
newObject.constructor;?//???Object()?{?[native?code]?}
如果操作不當(dāng)沒有正確遍歷對象屬性,可能會導(dǎo)致出現(xiàn)問題,產(chǎn)生你意料之外的 bug

const?countWords?=?(words)?=>?{
??const?counts?=?{?};
??for?(const?word?of?words)?{
????counts[word]?=?(counts[word]?||?0)?+?1;
??}
??return?counts;
};
const?counts?=?countWords(['constructor',?'creates',?'a',?'bug']);
//?{constructor:?"function?Object()?{?[native?code]?}1",?creates:?1,?a:?1,?bug:?1}
這個例子靈感來源于《Effective TypeScript》[1]一書
迭代器
「Map」 是可迭代的,可以直接進(jìn)行迭代,例如forEach循環(huán)或者for...of...循環(huán)
//forEach
const?map?=?new?Map();
map.set('key1',?'value1');
map.set('key2',?'value2');
map.set('key3',?'value3');
map.forEach((value,?key)?=>?{
??console.log(key,?value);
});
//?key1?value1
//?key2?value2
//?key3?value3
//for...of...
for(const?entry?of?map)?{
??console.log(entry);
}
//?["key1",?"value1"]
//?["key2",?"value2"]
//?["key3",?"value3"]
但是對于「Object」是不能直接迭代的,當(dāng)你嘗試迭代將導(dǎo)致報錯
const?object?=?{
??key1:?'value1',
??key2:?'value2',
??key3:?'value3',
};
for(const?entry?of?object)?{
??console.log(entry);
}
//?Uncaught?TypeError:?object?is?not?iterable
這時候你就需要一個額外的步驟來檢索其鍵名、鍵值或者鍵值對
for(const?key?of?Object.keys(object))?{
??console.log(key);
}
//?key1
//?key2
//?key3
for(const?value?of?Object.values(object))?{
??console.log(value);
}
//?value1
//?value2
//?value3
for(const?entry?of?Object.entries(object))?{
??console.log(entry);
}
//?["key1",?"value1"]
//?["key2",?"value2"]
//?["key3",?"value3"]
for(const?[key,value]?of?Object.entries(object))?{
??console.log(key,value);
}
//"key1",?"value1"
//"key2",?"value2"
//"key3",?"value3"
當(dāng)然也可以使用for...in...進(jìn)行遍歷循環(huán)鍵名
for(const?key?in?object)?{
??console.log(key);
}
//?key1
//?key2
//?key3
元素順序和長度
Map 保持對長度的跟蹤,使其能夠在O(1)復(fù)雜度中進(jìn)行訪問
const?map?=?new?Map();
map.set('key1',?'value1');
map.set('key2',?'value2');
map.set('key3',?'value3');
map.size;?//?3
而另一方面,對于「Object」而言,想要獲得對象的屬性長度,需要手動對其進(jìn)行迭代,使其為O(n)復(fù)雜度,屬性長度為n
在上文提及的示例中,我們可以看到「Map」始終保持按插入順序返回鍵名。但「Object」卻不是。從 ES6 開始,String和Symbol鍵是按順序保存起來的,但是通過隱式轉(zhuǎn)換保存成String的鍵就是亂序的
const?object?=?{?};
object['key1']?=?'value1';
object['key0']?=?'value0';
object;?//?{key1:?"value1",?key0:?"value0"}
object[20]?=?'value20';
object;?//?{20:?"value20",?key1:?"value1",?key0:?"value0"}
Object.keys(object).length;?//3
Object/Map 何為最佳實踐
如上就是 「Map」 和 「Object」 的基本區(qū)別,在解決問題考慮兩者的時候就需要考慮兩者的區(qū)別。
- 當(dāng)插入順序是你解決問題時需要考慮的,并且當(dāng)前需要使用除 String 和 Symbol 以外的鍵名時,那么 「Map」 就是個最佳解決方案
- 如果需要遍歷鍵值對(并且需要考慮順序),那我覺得還是需要優(yōu)先考慮 「Map」。
- Map是一個純哈希結(jié)構(gòu),而Object不是(它擁有自己的內(nèi)部邏輯)。Map?在頻繁增刪鍵值對的場景下表現(xiàn)更好,性能更高。因此當(dāng)你需要頻繁操作數(shù)據(jù)的時候也可以優(yōu)先考慮 Map
- 再舉一個實際的例子,比如有一個自定義字段的用戶操作功能,用戶可以通過表單自定義字段,那么這時候最好是使用 Map,因為很有可能會破壞原有的對象
const?userCustomFields?=?{
??'color':????'blue',
??'size':?????'medium',
??'toString':?'A?blue?box'
};
此時用戶自定義的 toString 就會破壞到原有的對象而 「Map」 鍵名接受任何類型,沒有影響
function?isMap(value)?{
??return?value.toString()?===?'[object?Map]';
}
const?actorMap?=?new?Map();
actorMap.set('name',?'Harrison?Ford');
actorMap.set('toString',?'Actor:?Harrison?Ford');
//?Works!
isMap(actorMap);?//?=>?true
- 當(dāng)你需要處理一些屬性,那么 「Object」 是完全受用的,尤其是需要處理 JSON 數(shù)據(jù)的時候。由于 「Map」 可以是任意類型,因此沒有可以將其轉(zhuǎn)化為 JSON 的原生方法。
var?map?=?new?Map()
map.set('key','value')
JSON.stringify(map)??//"{}"
- 當(dāng)你需要通正則表達(dá)式判斷去處理一些業(yè)務(wù)邏輯時,「Map」將是你的最佳解決方案
const?actions?=?()=>{
??const?functionA?=?()=>{/*do?sth*/}
??const?functionB?=?()=>{/*do?sth*/}
??const?functionC?=?()=>{/*send?log*/}
??returnnewMap([
????[/^guest_[1-4]$/,functionA],
????[/^guest_5$/,functionB],
????[/^guest_.*$/,functionC],
????//...
??])
}
const?onButtonClick?=?(identity,status)=>{
??let?action?=?[...actions()].filter(([key,value])=>(key.test(`${identity}_${status}`)))
??action.forEach(([key,value])=>value.call(this))
}
利用數(shù)組循環(huán)的特性,符合正則條件的邏輯都會被執(zhí)行,那就可以同時執(zhí)行公共邏輯和單獨(dú)邏輯,因為正則的存在,你可以打開想象力解鎖更多的玩法,更多相關(guān) Map 用法樣例可以查看JavaScript 復(fù)雜判斷的更優(yōu)雅寫法
總結(jié):
「Object」對象通??梢院芎玫谋4娼Y(jié)構(gòu)化數(shù)據(jù),但是也有相應(yīng)的局限性:
- 鍵名接受類型只能用 String 或者 Symbol
- 自定義的鍵名容易與原型繼承的屬性鍵名沖突(例如 toString,constructor 等)
- 對象/正則無法用作鍵名而這些問題通過 「Map」 都可以解決,并且提供了諸如迭代器和易于進(jìn)行大小查找之類的好處
不要將「Map」作為普通「Object」的替代品,而應(yīng)該是普通對象的補(bǔ)充
參考資料
[1]《Effective TypeScript》Dan Vanderkam
[2]https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map
[3]https://dmitripavlutin.com/maps-vs-plain-objects-javascript
https://medium.com/javascript-in-plain-english
?? 看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-)
關(guān)注我的官網(wǎng)?https://muyiy.cn,讓我們成為長期關(guān)系
關(guān)注公眾號「高級前端進(jìn)階」,公眾號后臺回復(fù)「面試題」 送你高級前端面試題,回復(fù)「加群」加入面試互助交流群
