重学JavaScript【Map和Set】

128 阅读7分钟

重学JavaScript 篇的目的是回顾基础,方便学习框架和源码的时候可以快速定位知识点,查漏补缺,所有文章都同步在 公众号(道道里的前端栈)github 上。

Map

ECMAScript6之前,在JavaScript中实现“键/值”形式存储可以使用Object开高效完成,也就是用对象的属性作为key,属性的值作为value。但是这种实现并非没有问题,所以TC39委员会专门为“键/值”存储定义了一个规范。

Map 是一种新的集合类型,为JavaScript带来了真正的键值存储机制。Map里面的大多数特性都可以通过Object来实现,但是二者还是会存在一些细微的差异。

API

Map构造函数可以哦通过 new 操作符创建一个空映射:

const m = new Map();

如果想在创建的同时初始化实例,可以传入一个可迭代对象,需要包含键值对数组:

const m = new Map([
  ["key", "value"]
]);
m.size // 1

初始化过后,可以使用 set() 方法再添加键值对,可以使用 get()has() 进行查询,可以通过 size 属性获取键值对的数量,可以使用 delete()clear() 删除值。

const m = new Map();
m.has("name"); // false
m.get("name"); // undefined

m.set("name", "abc")
 .set("age", 12);

m.has("name"); // true
m.get("name"); // "abc"
m.size // 2

m.delete("name");
a.size // 1

m.clear();
m.size // 0

由于set方法返回的是映射实例,所以可以用 . 来进行链式操作。

在Object中,键只能使用数字,字符串和符号,但是在Map中可以使用任何JavaScript数据类型作为键,其映射的值也是没有限制的。

const m = new Map();
const fnKey = () => {};
const symbolKey = () => {};
const objKey = () => {};

m.set(fnKey, "fnValue")
 .set(symbolKey, "symbolValue")
 .set(objKey, "objValue");

m.get(fnKey); //fnValue
m.get(symbolKey); //symbolValue
m.get(objKey); // objValue

在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变。

const m = new Map(); 
const objKey = {}, 
 objVal = {}, 
 arrKey = [], 
 arrVal = []; 
m.set(objKey, objVal); 
m.set(arrKey, arrVal); 

objKey.foo = "foo"; 
objVal.bar = "bar"; 
arrKey.push("foo"); 
arrVal.push("bar"); 

console.log(m.get(objKey)); // {bar: "bar"} 
console.log(m.get(arrKey)); // ["bar"]

顺序和迭代

和Object的一个主要差异是,Object中没有插入顺序,而Map会维护键值对的插入顺序,从而可以根据插入顺序执行迭代操作。

映射的实例提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组,可以通过entries() 方法(或者 Symbol.iterator属性,因为它引用了entries)取得这个迭代器:

const m = new Map([
  ["k", "val"]
]);
m.entries === m[Symbol.iterator]; // true

for (let c of m.entries()){
  console.log(c);
};
// ["k", "val"]

由于entries()是默认的迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:

[...m] // [["k", "val"]]

当然也可以使用forEach等方法进行操作。

keys()values() 分别返回以插入顺序生成键值的迭代器:

for (let key of m.keys()) { 
 console.log(key)
}
// k

for (let value of m.values()) { 
 console.log(value)
}
// val

注意:在Map里面,遍历的时候是可以修改键值的,但是在映射内部的引用无法修改

const m = new Map([
  ["k", "val"]
]);

for(let key of m.keys()){
  key = "newK";
  console.log(key, m.get("k"));
};
console.log(m.entries());
// newK val
// MapIterator {"k" => "val"}

for(let key of m.values()){
  key = "newV";
  console.log(key, m.get("k"));
};
console.log(m.entries());
// newV val
// MapIterator {"k" => "val"}

WeakMap

ECMAScript6中新增的“弱映射”(WeakMap)是一种新的集合类型,它是Map的兄弟类型,其API也是Map的子集,WeakMap中的“weak”,描述的就是JavaScript垃圾回收对待“弱映射”中键的方式。

API

使用new关键字实例化一个空的WeakMap:

const wm = new WeakMap();

在弱映射中,键只能是Object或者继承自Object的类型,如果使用其他类型会报TypeError,值的类型没有限制。

const key = {id: 1}
const wm = new WeakMap([
  [key, "value"]
]);
wm.get(key); // "value"

在初始化之后可使用 set() 添加键值对,使用 get()has() 查询,使用 delete() 删除:

const wm = new WeakMap(); 
const key1 = {id: 1}, 
 key2 = {id: 2}; 
wm.has(key1); // false 
wm.get(key1); // undefined 

wm.set(key1, "Matt") 
  .set(key2, "Frisbie"); 
wm.has(key1); // true 
wm.get(key1); // Matt 

wm.delete(key1); // 只删除这一个键/值对
wm.has(key1); // false 
wm.has(key2); // true

WeakMap的键所指向的对象,不计入垃圾回收机制。

那为什么要设计出来一个WeakMap呢?这里引用阮一峰的例子,请看:

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];

上面代码中,e1和e2是两个对象,通过arr对这两个对象加了一些说明,此时arr引用了e1和e2,一旦不在需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放e1和e2:

arr[0] = null;
arr[1] = null;

这种写法很容易被我们忘记,从而造成内存泄漏。

WeakMap就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,垃圾回收机制不会考虑它们,只要所引用的对象被清除,垃圾回收机制就会释放该对象所占用的内存,不用我们手动删除引用。

WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失,WeakMap结构有助于防止内存泄漏。

注意:WeapMap弱引用的是键名,键值依然是正常引用。

const wm = new WeakMap();
let key = {};
let obj = {foo: 1};

wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}

选择Object还是Map

对于在乎内存和性能来说,大致有以下几点区别:

  1. 内存占用

    当浏览器给定固定大小的内存时,Map比Object多存储50%的键值对。

  2. 插入性能

    插入场景下,Map会比Object快一点,如果涉及大量插入操作,优先使用Map。

  3. 查找速度

    Object和Map在查找速度条件下,差异不大,如果包含少量的键值对,请使用Object

  4. 删除性能

    一般情况下,delete不是很常用,更多的是赋值为undefined或者null,有大量删除操作的话,Map的delete操作要快很多

Set

Set 可以理解为加强版的Map,它们的大多是API和行为是共有的。

API

Set也是通过new关键字来创建的:

const m = new Set();

初始化传入的参数也是个可迭代对象,其中包括插入到新集合实例中的元素:

const s = new Set(["foo", "bar"]);
s.size // 3

初始化之后,可以使用 add() 来增加值,使用 has() 来查询,使用 size 获得元素数量,使用 delete()clear() 删除元素:

const s = new Set();
s.has("name"); // false
s.size; // 0

s.add("name")
 .add("age");
s.has("name"); // true
s.size; // 2

s.delete("name");
s.has("name"); //false

s.clear();
s.size; // 0

上面的add就像Map中的set方法一样。Set可以包含任何JavaScript数据类型作为值:

const s = new Set(); 
const functionVal = () => {}; 
const symbolVal = Symbol(); 
const objectVal = new Object(); 

s.add(functionVal); 
s.add(symbolVal); 
s.add(objectVal); 

s.has(functionVal); // true 
s.has(symbolVal); // true 
s.has(objectVal); // true 

同样的,用作值的对象和其他“集合”类型在自己的内容或属性被修改时也不会被改变:

const s = new Set(); 
const objVal = {}, 
 arrVal = []; 

s.add(objVal); 
s.add(arrVal); 

objVal.bar = "bar"; 
arrVal.push("bar"); 

s.has(objVal); // true 
s.has(arrVal); // true

Set的delete方法返回的是一个布尔值,表示集合中是否存在要删除的值:

const s = new Set(); 
s.add('foo'); 
s.size; // 1 
s.add('foo'); 
s.size; // 1 

// 集合里有这个值
s.delete('foo'); // true 
// 集合里没有这个值
s.delete('foo'); // false

迭代

Set的迭代和Map很类似,同样它也可以直接对集合实例进行扩展:

const s = new Set(["foo", "bar"]);
[...s] // ["foo", "bar"]

其他的有关迭代器的用法,和Map一样。

WeakSet

WeakSet 就是Set对应的兄弟类型,它是一个弱集合,用法和Set类似,也和WeakMap类似。

它里面的值只能是Object或继承自Object的类型,否则会报TypeError。

const val1 = {id: 1}, 
 val2 = {id: 2}, 
 val3 = {id: 3};
const ws = new WeakSet([val1, val2, val3]);
ws.has(val1); // true
ws.has(val2); // true
ws.has(val3); // true

它也支持add,has,delete,用法和Set一样。

至于垃圾回收方面,它和WeakMap是一样的。

总结

Set和Map主要的应用场景在于 数据重组数据存储

  • Set

    • 内部类似于数组,成员唯一且无序
    • [value, value],键值和键名一样,也可以理解为只有键值,没有键名
    • 可以遍历,有add,delete,has,clear
  • WeakSet

    • 成员都是对象
    • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM
    • 不能遍历,方法有add,delete,has,clear
  • Map

    • 本质上就是键值对的集合,类似集合,[key,value]
    • 可以遍历,方法有get,set,has,delete,clear
  • WeakMap

    • 只接受对象作为键名(null除外)
    • 键名是弱引用,键值任意,键名指向的对象可以被垃圾回收
    • 不能遍历,方法有get,set,has,delete,clear*

我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~