这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
ECMAScript 6 以前,在 JavaScript 中实现“键/值”式存储可以使用 Object 来方便高效地完成,也就是使用对象属性作为键,再使用属性来引用值。
但这种实现并非没有问题,Object只能使用数值、字符串或符号作为键,因此在将对象作为键时,object会将对象强制转换成字符串形式(调用其toString),再将其作为键值,如object => [object Object],这使得使用不同对象来映射不同值成为不可能。
Map 是 ES6 新增的一种新的集合类型,它是一种真正的键/值存储机制。Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。具体实践中使用哪一个,还是值得细细甄别。
基本API
Map提供了一系列初始化、操作、遍历的方法来方便我们使用。
初始化
Map是一个构造函数,需要使用new来创建示例
let map = new Map();//Map(0) {}
如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含键/值对数组。可迭代对象中的每个键/值对都会按照迭代顺序插入到实例中:
// 使用嵌套数组初始化映射
const m1 = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
>>> Map(3) {"key1" => "val1", "key2" => "val2", "key3" => "val3"}
// 使用自定义迭代器初始化映射
const m2 = new Map({
[Symbol.iterator]: function*() {
yield ["key1", "val1"];
yield ["key2", "val2"];
yield ["key3", "val3"];
}
});
>>> Map(3) {"key1" => "val1", "key2" => "val2", "key3" => "val3"}
操作
Map 的五大操作方法:set、get、has、delete、clear
size
map对象的一个内部属性,返回元素的数量,该属性是只读的,因此不能像数组通过修改此值来修改大小。用set 方法修改size返回undefined
let myMap = new Map();
myMap.set("a", "alpha");
myMap.set("b", "beta");
myMap.set("g", "gamma");
myMap.size // 3
set
添加或更新一个指定了键(key)和值(value)的(新)键值对,并返回自身,因此可链式调用。
let myMap = new Map();
// 将一个新元素添加到 Map 对象
myMap.set("bar", "foo");
myMap.set(1, "foobar");
// 在Map对象中更新某个元素的值
myMap.set("bar", "baz");
//链式调用
myMap.set('bar', 'foo')
.set(1, 'foobar')
.set(2, 'baz');
get
返回 Map 中的指定元素。
let myMap = new Map();
myMap.set("bar", "foo");
myMap.get("bar"); // 返回 "foo"
myMap.get("baz"); // 返回 undefined
has
返回一个 bool 值,用来表明 map 中是否存在指定元素。
let myMap = new Map();
myMap.set("bar", "foo");
myMap.has("bar"); // returns true
myMap.has("baz"); // returns false
delete
移除 Map 对象中指定的元素。
let myMap = new Map();
myMap.set("bar", "foo");
myMap.delete("bar"); // 返回 true。成功地移除元素
myMap.has("bar"); // 返回 false。"bar" 元素将不再存在于 Map 实例中
clear
移除 Map 对象中的所有元素,返回值是undefined
let myMap = new Map();
myMap.set("bar", "baz");
myMap.set(1, "foo");
myMap.size; // 2
myMap.has("bar"); // true
myMap.clear();
myMap.size; // 0
myMap.has("bar") // false
遍历
Map 提供了一些方法来返回迭代器对象,方便我们使用for...of、forEach进行遍历
keys
返回一个引用的 Iterator 对象。它包含按照顺序插入 Map 对象中每个元素的 key 值。
let myMap = new Map();
myMap.set("0", "foo");
myMap.set(1, "bar");
myMap.set({}, "baz");
let mapIter = myMap.keys();
console.log(mapIter.next().value); // "0"
console.log(mapIter.next().value); // 1
console.log(mapIter.next().value); // Object
for(let i of mapIter)
console.log(i)
values
返回一个新的Iterator对象。它包含按顺序插入Map对象中每个元素的 value 值。
let myMap = new Map();
myMap.set("0", "foo");
myMap.set(1, "bar");
myMap.set({}, "baz");
let mapIter = myMap.values();
console.log(mapIter.next().value); // "foo"
console.log(mapIter.next().value); // "bar"
console.log(mapIter.next().value); // "baz"
entires
返回一个新的包含 [key, value] 对的 Iterator 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同。
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
for (let pair of m.entries()) {
console.log(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
m.forEach((val, key) => console.log(`${key} -> ${val}`));
// key1 -> val1
// key2 -> val2
// key3 -> val3
与object的区别
键的类型
Map 的键可以是任意值,包括函数、对象或任意基本类型。 Object 的键只能是 String 或是 Symbol ,将除这2种类型外的类型作为键时,内部会将其转换成字符串再作为键。
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')
const obj = {}
const map = new Map()
// dom节点对象作为Object的键值时,会被转换成字符串(调用其toString),将值[object HTMLDivElement]作为键
// 因此div1的值被div2覆盖了
obj[div1] = 'div1'
obj[div2] = 'div2'
console.log(obj)//{ [object HTMLDivElement]: "div2" }
// map直接将dom节点作为键
map.set(div1,"div1")
map.set(div2,"div2")
console.log(map)//{ div#div1 => "div1", div#div2 => "div2" }
键的顺序
Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。Object 的键是无序的
自ECMAScript 2015规范以来,对象确实保留了字符串和Symbol键的创建顺序; 因此,在只有字符串键的对象上进行迭代将按插入顺序产生键。---MDN
[]运算符
Map和Object都能使用[]运算符,但是效果不一样。
const map = new Map()
const obj = new Object()
map['name']="jalenl" //Map(1){name:"Jalen"}
map.has('name')//false
map.get('name')//undefined
map.set('name','jalenl')// Map(1){"name" => "Jalen"}
map.has('name')//true
obj['name']="jalenl" //{name:"Jalen"}
Map和Object使用[]修改的是自身的对象属性,但对于Map来说,自身的属性和元素没有任何关系,size()得到的元素数量不变。
但Map之所以能使用[]运算符,是因为其原型链最底层就是Object,它是从Object继承来的。
const map = new Map()
map instanceof Object//true
自带的键
Map 默认情况不包含任何键。只包含显式插入的键。一个 Object 实例有一个原型, 原型链上的键名有可能和自定义设置的键名产生冲突。
ES5可以用 Object.create(null) 来创建一个没有原型的对象。
迭代器
Map内置迭代器对象,其默认迭代器是entries(),Object没有内置迭代器。因此for...of可直接用于map实例,而object实例不可以,必须为object实例设置迭代器对象。
String、Array、TypedArray、Map和Set都是内置可迭代对象,因为它们的原型对象都拥有一个Symbol.iterator方法。
const obj ={
name:"jalenl",
age: 18
}
const map = new Map([["name","jalenl"],["age","18"]])
console.log(map[Symbol.iterator])//[Function: entries]
console.log(obj[Symbol.iterator])//undefined
如何选择
对于普通开发任务来说,选择 Object 或 Map 看个人偏好,但在内存管理和性能上,它俩有显著差别。
- 内存占用
Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量
都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。
不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。
- 插入性能
向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快
一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操
作,那么显然 Map 的性能更佳。
- 查找速度
与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,
则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏
览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,
查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选
择 Object 更好一些。
- 删除性能
使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,
出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。但很多时候,这都是一
种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。
如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。