JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),可是传统上只能用字符串看成键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它相似于对象,也是键值对的集合,可是“键”的范围不限于字符串,各类类型的值(包括对象)均可以看成键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。若是你须要“键值对”的数据结构,Map 比 Object 更合适。算法
ES6 的 Map 类型是键值对的有序列表,而键和值均可以是任意类型。 键的比较使用的是Object.is() ,所以你能将 5 与 "5"
同时做为键,由于它们类型不一样。这与使用对象属性做为键的方式(指的是用对象来模拟 Map )大相径庭,由于对象的属性会被强制转换为字符串。
你能够调用 set() 方法并给它传递一个键与一个关联的值,来给 Map 添加项;此后使用键名来调用 get() 方法便能提取对应的值。例如:json
let map = new Map(); map.set("title", "Understanding ES6"); map.set("year", 2016); console.log(map.get("title")); // "Understanding ES6" console.log(map.get("year")); // 2016
依然与 Set 相似,你能将数组传递给 Map 构造器,以便使用数据来初始化一个 Map 。该数组中的每一项也必须是数组,内部数组的首个项会做为键,第二项则为对应值。所以整个Map 就被这些双项数组所填充。例如:数组
let map = new Map([["name", "Nicholas"], ["age", 25]]); console.log(map.has("name")); // true console.log(map.get("name")); // "Nicholas" console.log(map.size); // 2
经过构造器中的初始化, "name" 与 "age" 这两个键就被添加到 map 变量中。虽然由数组构成的数组看起来有点奇怪,这对于准确表示键来讲倒是必要的:由于键容许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 以前不会被强制转换为其余类型的惟一方法。
Map构造函数接受数组做为参数,实际上执行的是下面的算法。浏览器
const items = [ ["name", "Nicholas"] ["age", 25] ] const map = new Map(); items.forEach( ([key, value]) => map.set(key, value) );
事实上,不只仅是数组,任何具备 Iterator 接口、且每一个成员都是一个双元素的数组的数据结构均可以当作Map构造函数的参数。这就是说,Set和Map均可以用来生成新的Map缓存
const set = new Set([ ['foo', 1], ['bar', 2] ]); const m1 = new Map(set); m1.get('foo') // 1 const m2 = new Map([['baz', 3]]); const m3 = new Map(m2); m3.get('baz') // 3
上面代码中,咱们分别使用 Set 对象和 Map 对象,看成Map构造函数的参数,结果都生成了新的 Map 对象。数据结构
Map 一样拥有 size 属性,用于指明包含了多少个键值对函数
set方法设置键名key对应的键值为value,而后返回整个 Map 结构。若是key已经有值,则键值会被更新,不然就新生成该键。优化
const m = new Map(); m.set('edition', 6) // 键是字符串 m.set(262, 'standard') // 键是数值 m.set(undefined, 'nah') // 键是 undefined
set方法返回的是当前的Map对象,所以能够采用链式写法。设计
let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c');
get方法读取key对应的键值,若是找不到key,返回undefined。code
const m = new Map(); const hello = function() {console.log('hello');}; m.set(hello, 'Hello ES6!') // 键是函数 m.get(hello) // Hello ES6!
Map 与 Set 共享了几个方法,这是有意的,容许你使用类似的方式来与 Map 及 Set 进行交互。如下三个方法在 Map 与 Set
上都存在:
has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
const m = new Map(); m.set('edition', 6); m.set(262, 'standard'); m.set(undefined, 'nah'); m.has('edition') // true m.has('years') // false m.has(262) // true m.has(undefined) // true
delete方法删除某个键,返回true。若是删除失败,返回false。
const m = new Map(); m.set(undefined, 'nah'); m.has(undefined) // true m.delete(undefined) m.has(undefined) // false
clear方法清除全部成员,没有返回值。
let map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2 map.clear() map.size // 0
• keys():返回键名的遍历器。
• values():返回键值的遍历器。
• entries():返回全部成员的遍历器
须要特别注意的是,Map 的遍历顺序就是插入顺序。
const map = new Map([ ['F', 'no'], ['T', 'yes'], ]); for (let key of map.keys()) { console.log(key); } // "F" // "T" for (let value of map.values()) { console.log(value); } // "no" // "yes" for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" // 或者 for (let [key, value] of map.entries()) { console.log(key, value); } // "F" "no" // "T" "yes" // 等同于使用map.entries() for (let [key, value] of map) { console.log(key, value); } // "F" "no" // "T" "yes"
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
Map 的 forEach() 方法相似于 Set 与数组的同名方法,它接受一个能接收三个参数的回调函数:
回调函数的这些参数更紧密契合了数组 forEach() 方法的行为,即:第一个参数是值、第二个参数则是键(数组中的键是数值索引)。此处有个示例:
let map = new Map([ ["name", "Nicholas"], ["age", 25] ]); map.forEach(function(value, key, ownerMap) { console.log(key + " " + value); console.log(ownerMap === map); });
forEach() 的回调函数输出了传给它的信息。其中 value 与 key 被直接输出, ownerMap
与 map 进行了比较,说明它们是相等的。这就输出了:
name Nicholas true age 25 true
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。
const map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); [...map.keys()] // [1, 2, 3] [...map.values()] // ['one', 'two', 'three'] [...map.entries()] // [[1,'one'], [2, 'two'], [3, 'three']] [...map] // [[1,'one'], [2, 'two'], [3, 'three']]
将数组传入 Map 构造函数,就能够转为 Map。
new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { // true => 7, // Object {foo: 3} => ['abc'] // }
若是全部 Map 的键都是字符串,它能够无损地转为对象。
function strMapToObj(strMap) { let obj = Object.create(null); for (let [k,v] of strMap) { obj[k] = v; } return obj; } const myMap = new Map() .set('yes', true) .set('no', false); strMapToObj(myMap) // { yes: true, no: false }
若是有非字符串的键名,那么这个键名会被转成字符串,再做为对象的键名。
function objToStrMap(obj) { let strMap = new Map(); for (let k of Object.keys(obj)) { strMap.set(k, obj[k]); } return strMap; } objToStrMap({yes: true, no: false}) // Map {"yes" => true, "no" => false}
Map 转为 JSON 要区分两种状况。一种状况是,Map 的键名都是字符串,这时能够选择转为对象 JSON。
function strMapToJson(strMap) { return JSON.stringify(strMapToObj(strMap)); } let myMap = new Map().set('yes', true).set('no', false); strMapToJson(myMap) // '{"yes":true,"no":false}'
另外一种状况是,Map 的键名有非字符串,这时能够选择转为数组 JSON。
function mapToArrayJson(map) { return JSON.stringify([...map]); } let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']); mapToArrayJson(myMap) // '[[true,7],[{"foo":3},["abc"]]]'
JSON 转为 Map,正常状况下,全部键名都是字符串。
function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } jsonToStrMap('{"yes": true, "no": false}') // Map {'yes' => true, 'no' => false}
可是,有一种特殊状况,整个 JSON 就是一个数组,且每一个数组成员自己,又是一个有两个成员的数组。这时,它能够一一对应地转为 Map。这每每是 Map 转为数组 JSON 的逆操做。
function jsonToMap(jsonStr) { return new Map(JSON.parse(jsonStr)); } jsonToMap('[[true,7],[{"foo":3},["abc"]]]') // Map {true => 7, Object {foo: 3} => ['abc']}
WeakMap与Map的区别有两点。
首先,WeakMap只接受对象做为键名(null除外),不接受其余类型的值做为键名。
const map = new WeakMap(); map.set(1, 2) // TypeError: 1 is not an object! map.set(Symbol(), 2) // TypeError: Invalid value used as weak map key map.set(null, 2) // TypeError: Invalid value used as weak map key
其次,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占用的内存。
// 不须要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null; arr [1] = null;
上面这样的写法显然很不方便。一旦忘了写,就会形成内存泄露。
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。所以,只要所引用的对象的其余引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦再也不须要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
ES6 的 WeakMap 类型是键值对的无序列表,其中键必须是非空的对象,值则容许是任意类型。 WeakMap 的接口与 Map 的很是类似
// WeakMap 可使用 set 方法添加成员
const wm1 = new WeakMap(); const key = {foo: 1}; wm1.set(key, 2); wm1.get(key) // 2
// WeakMap 也能够接受一个数组,
// 做为构造函数的参数
const k1 = [1, 2, 3]; const k2 = [4, 5, 6]; const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]); wm2.get(k2) // "bar"
WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操做(即没有keys()、values()和entries()方法),也没有size属性。由于没有办法列出全部键名,某个键名是否存在彻底不可预测,跟垃圾回收机制是否运行相关。这一刻能够取到键名,下一刻垃圾回收机制忽然运行了,这个键名就没了,为了防止出现不肯定性,就统一规定不能取到键名。二是没法清空,即不支持clear方法。所以,WeakMap只有四个方法可用:get()、set()、has()、delete()。
const wm = new WeakMap(); // size、forEach、clear 方法都不存在 wm.size // undefined wm.forEach // undefined wm.clear // undefined
Weak Map 的最佳用武之地,就是在浏览器中建立一个关联到特定 DOM 元素的对象。例如,某些用在网页上的 JS 库会维护一个自定义对象,用于引用该库所使用的每个 DOM 元素,而且其映射关系会存储在内部的对象缓存中。
该方法的困难之处在于:如何判断一个 DOM 元素已不复存在于网页中,以便该库能移除此元素的关联对象。若作不到,该库就会继续保持对 DOM 元素的一个无效引用,并形成内存泄漏。使用 Weak Map 来追踪 DOM 元素,依然容许将自定义对象关联到每一个 DOM 元素,而在此对象所关联的 DOM 元素不复存在时,它就会在 Weak Map 中被自动销毁。
必须注意的是, Weak Map 的键才是弱引用,而值不是。在 Weak Map 的值中存储对象会阻止垃圾回收,即便该对象的其余引用已全都被移除。
当决定是要使用 Weak Map 仍是使用正规 Map 时,首要考虑因素在于你是否只想使用对象类型的键。若是你打算这么作,那么最好的选择就是 Weak Map 。由于它能确保额外数据在再也不可用后被销毁,从而能优化内存使用并规避内存泄漏。 要记住 Weak Map 只为它们的内容提供了很小的可见度,所以你不能使用 forEach() 方法、size 属性或 clear() 方法来管理其中的项。若是你确实须要一些检测功能,那么正规 Map会是更好的选择,只是必定要确保留意内存的使用。