JavaScript 映射
映射 Map
是 ES 6 新增的一种特殊的内置对象,它是一个键值对的数据项的集合,通过构造函数 Map()
创建一个(空)映射,使用方法 .set(key, value)
向映射添加键值对,使用方法 .get(key)
通过键 key
访问相应的值。
let map = new Map();
map.set('1', 'str1'); // 字符串键
map.set(1, 'num1'); // 数字键
map.set(true, 'bool1'); // 布尔值键
// 普通的 Object 会将键转化为字符串
// Map 则会保留键的类型,所以下面这两个结果不同
alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1'
alert( map.size ); // 3
Tip
使用构造函数 Map()
创建映射时,可以传入一个 带有键值对的数组(或其它可迭代对象) 来进行初始化
// 键值对 [key, value] 数组
let map = new Map([
['1', 'str1'],
[1, 'num1'],
[true, 'bool1']
]);
alert( map.get('1') ); // str1
Tip
与一般对象最大的差别是 Map
的键 key
可以任何类型的数据,而对象的键只能是字符串。使用对象作为键是 Map 最值得注意和重要的功能之一。
let john = { name: "John" };
// 存储每个用户的来访次数
let visitsCountMap = new Map();
// john 是 Map 中的键
visitsCountMap.set(john, 123);
alert( visitsCountMap.get(john) ); // 123
Tip
甚至 NaN
也可以被用作键,Map
使用 SameValueZero 算法来比较键是否相等。它和严格等于 ===
差不多,但区别是 NaN
被看成是等于 NaN
。
Warning
map[key]
不是使用 Map
的正确方式,虽然也有效,但这样会将 map
视为 JavaScript 的 plain object(有所限制,如没有对象键等)。应该使用 map
特有的方法 .set(key, value)
为映射添加元素,用方法 .get(key)
访问键相应的值。
Tip
使用方法 .get(key)
传入一个键,检索并返回 Map 中相应的 value
值,如果 map
中不存在对应的 key
,则返回 undefined
。
映射长度
属性 size
返回当前映射中的元素个数。
修改映射
- 方法
.set(key, value)
向映射添加键值对。该方法接受两个参数,第一个参数是key
键,用来引用第二个参数value
即值。如果添加键已存在的键值对,不会收到错误,但会覆盖原有的键值对。成功执行,则返回 Map 对象本身。 - 方法
.delete(key)
移除相应的键值对,当尝试删除的键值对并不存在时不会收到错误,映射保持不变 - 方法
.clear()
清空映射,删除所有键值对,删除成功返回true
,失败则返回false
let employees = new Map();
employees.set('[email protected]', {
firstName: 'James',
lastName: 'Parkes',
role: 'Content Developer'
});
employees.set('[email protected]', {
firstName: 'Julia',
lastName: 'Van Cleve',
role: 'Content Developer'
});
console.log(employees); \\Map {'[email protected]' => Object {...}, '[email protected]' => Object {...}}
Tip
每一次 map.set
调用都会返回 map
本身,所以我们可以进行「链式」调用为映射连续添加多个元素
let map = new Map();
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
检查键
使用方法 .has(key)
向其传入一个键名,检查 Map 中是否存在该键值对,返回 true
或 false
。
迭代
可以通过三种方式循环访问映射的键、值、键值对
- 方法
.keys()
返回一个映射的键迭代器,结合 for-of 循环结构遍历所有键 - 方法
.values()
返回一个映射的值迭代器,结构 for-of 循环结构遍历所有值 - 方法
.entries()
返回一个实体/键值对组成的数组的迭代器,结合 for-of 结构使用(映射自身就是一个可迭代对象,在循环结构 for-of 中默认返回的迭代器就是键值对迭代器),💡 可以使用数组解构来提取相应的数据
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 遍历所有的键(vegetables)
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// 遍历所有的值(amounts)
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// 遍历所有的实体 [key, value]
for (let entry of recipeMap) { // 默认情况,与 recipeMap.entries() 相同
alert(entry); // cucumber,500 (and so on)
}
Tip
映射 Map
与数组类似,也内置的方法 .forEach( fucntion(value, key, map) )
循环读取每个键值对,并依次执行回调函数的处理。
回调函数可设置三个参数
value
元素的值key
元素的键Map
当前正在被遍历的对象
Tip
迭代器除了可以用于 for-of 循环结构中,还可以调用方法 .next()
手动一步一步循环访问每个数据
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 使用迭代器和方法 nexty() 手动循环访问映射的键
let iteratorObjForKeys = members.keys();
iteratorObjForKeys.next(); // Object {value: 'cucumber', done: false}
// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 etc
});
Warning
在 Map 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。
Warning
调用方法 .keys()
、.values()
、.entries()
返回的是可迭代对象,而非数组,可以使用 Array.from(Iterable)
转换为一个数组,以使用数组的特有方法。
let map = new Map();
map.set("name", "John");
let keys = Array.from(map.keys());
keys.push("more");
alert(keys); // name, more
从对象创建 Map
使用构造函数 Map()
创建映射时,可以传入一个带有键值对的数组(或其它可迭代对象)来进行初始化。
函数 Object.entries(obj)
基于传递进入的普通对象 obj
返回一个数组,该数组的格式按照 Map()
初始化所需的格式(即元素是由键值对组成的),通过转变对象就可以用来创建映射。
let obj = {
name: "John",
age: 30
};
let arrKeyValue = Object.entries(obj); // [ ["name","John"], ["age", 30] ]。
let map = new Map(arrKeyValue);
alert( map.get('name') ); // John
从 Map 创建对象
类似地,函数 Object.fromEntries(arr)
可以根据给定的数组(其元素时键值对 [key, value]
形式)返回一个普通对象。
先利用 Map 迭代器 .entries()
生成包含所有键值对的数组,再结合 Object.formEntries(arr)
即可生成相应的普通对象
let map = new Map();
map.set('banana', 1);
map.set('orange', 2);
map.set('meat', 4);
let obj = Object.fromEntries(map.entries()); // 创建一个普通对象(plain object)(*)
// 完成了!
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2
Tip
其实函数 Object.fromEntries()
期望得到一个可迭代对象作为参数(不一定是数组),映射 map
本身就是一个可迭代对象(默认迭代器与 map.entries()
生成的迭代器相同),因此可以直接传递 map
让代码更简洁
let obj = Object.fromEntries(map); // 省掉 .entries()
弱映射
根据 垃圾回收 规则可知 JavaScript 引擎在值可访问(并可能被使用)时将其存储在内存中,并不会将空间进行回收,⚠️ 通常当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是可以访问的(即使相应的变量/主存储器已经被覆盖),这会造成回收清理很麻烦。
let john = { name: "John" }; // 该对象能被访问,john 是它的引用
// 覆盖引用
john = null; // 该对象将会被从内存中清除
// 当对象引用在数组结构中
let Ben = { name: "Ben" };
let array = [ Ben ];
// 覆盖引用
Ben = null;
// john 被存储在数组里, 所以它不会被垃圾回收机制回收
array[0]; // {name: "Ben"} 仍然可以通过 array[0] 来获取它
如果我们使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在(即使引用该对象的变量已经被重置,该内存空间也不会被回收),这会造成无用资源占用内存的问题,可以使用另一种称为 WeakMap
弱映射的数据结构代替,以解决垃圾回收的问题。
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆盖引用
// john 被存储在 map 中,
// 仍可以使用 map.keys() 来获取它
WeakMap
与 Map
很像,最根本的不同是它不会阻止垃圾回收机制对作为键的对象 key object 的回收以释放内存空间。
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 覆盖引用
// john 被从内存中删除了!
WeakMap 和 Map 很像,但具有以下关键区别:
WeakMap
的键必须是对象数据类型,如果你尝试其他数据类型作为键系统将报错WeakMap
不支持迭代以及keys()
,values()
和entries()
方法。所以没有办法获取WeakMap
的所有键或值。WeakMap
只有以下的方法:weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
- 不支持
size
属性,没有.clear()
方法
Tip
WeakMap
弱映射不支持引用所有键或其计数的方法和属性,仅允许单个操作。这是技术的原因,如果一个对象丢失了其它所有引用(就像上面示例中的 john
),那么它就会被垃圾回收机制自动回收。但是在从技术的角度并不能准确知道 何时会被回收。这些都是由 JavaScript 引擎决定的。JavaScript 引擎可能会选择立即执行内存清理,如果现在正在发生很多删除操作,那么 JavaScript 引擎可能就会选择等一等,稍后再进行内存清理。因此,从技术上讲,WeakMap
的当前元素的数量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分。因此,暂不支持访问 WeakMap
的所有键/值的方法。
WeakMap
的主要应用场景是 额外数据的存储,以对象作为键,而对象相关的信息作为值,将这些键值对数据到 WeakMap 弱映射中,那么当该对象被垃圾回收机制回收后,与对象相关的数据也会被自动清除。
// 记录用户访问次数
// 以用户对象作为键,其访问次数为值。当一个用户离开时这时就不再需要他的访问次数了(该用户对象将被垃圾回收机制回收)
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// 递增用户来访次数
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
let john = { name: "John" };
countUser(john); // count his visits
// 不久之后,john 离开了
john = null; // 当 john 对象变成不可访问时,它也会连同它作为 WeakMap 里的键所对应的信息一同被从内存中删除
另外一个普遍的例子是缓存:当一个函数的结果需要被记住/缓存,这样在后续的对同一个对象的调用时,就可以重用这个被缓存的结果;而如果主存储器被重置(对象引用都被删除以后),存储在弱映射中以该对象作为键的数据就会被删除
// 📁 cache.js
let cache = new WeakMap();
// 计算并记结果
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculate the result for */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
// 在其它文件中使用 process()
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// ……稍后,我们不再需要这个对象时:
obj = null;
// 无法获取 cache.size,因为它是一个 WeakMap,要么是 0,或即将变为 0
// 当 obj 被垃圾回收「缓存」的数据也会被清除
Tip
WeakMap 一般被用作「主要」对象存储之外的「辅助」数据结构。一旦将对象从主存储器(变量引用被重置)中删除,如果该对象仅被用作 WeakMap 的键,那么它将被自动清除。