真正的键值存储机制-Map

837 阅读7分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

ECMAScript 6 以前,在 JavaScript 中实现“键/值”式存储可以使用 Object 来方便高效地完成,也就是使用对象属性作为键,再使用属性来引用值。

但这种实现并非没有问题,Object只能使用数值、字符串或符号作为键,因此在将对象作为键时,object会将对象强制转换成字符串形式(调用其toString),再将其作为键值,如object => [object Object],这使得使用不同对象来映射不同值成为不可能。

MapES6 新增的一种新的集合类型,它是一种真正的键/值存储机制。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 的五大操作方法:setgethasdeleteclear

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...offorEach进行遍历

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

[]运算符

MapObject都能使用[]运算符,但是效果不一样。

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"}

MapObject使用[]修改的是自身的对象属性,但对于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实例设置迭代器对象。

StringArrayTypedArrayMapSet 都是内置可迭代对象,因为它们的原型对象都拥有一个 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

如何选择

对于普通开发任务来说,选择 ObjectMap 看个人偏好,但在内存管理和性能上,它俩有显著差别。

  1. 内存占用

ObjectMap 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量 都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。 不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。

  1. 插入性能

ObjectMap 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快 一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操 作,那么显然 Map 的性能更佳。

  1. 查找速度

与插入不同,从大型 ObjectMap 中查找键/值对的性能差异极小,但如果只包含少量键/值对, 则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏 览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言, 查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选 择 Object 更好一些。

  1. 删除性能

使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此, 出现了一些伪删除对象属性的操作,包括把属性值设置为 undefinednull。但很多时候,这都是一 种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Mapdelete()操作都比插入和查找更快。 如果代码涉及大量删除操作,那么毫无疑问应该选择 Map