重学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
对于在乎内存和性能来说,大致有以下几点区别:
- 内存占用
当浏览器给定固定大小的内存时,Map比Object多存储50%的键值对。 - 插入性能
插入场景下,Map会比Object快一点,如果涉及大量插入操作,优先使用Map。 - 查找速度
Object和Map在查找速度条件下,差异不大,如果包含少量的键值对,请使用Object - 删除性能
一般情况下,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*