重学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*
我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~