JavaScript 映射

javascript

JavaScript 映射

映射 Map 是 ES 6 新增的一种特殊的内置对象,它是一个键值对的数据项的集合,通过构造函数 Map() 创建一个(空)映射,使用方法 .set(key, value) 向映射添加键值对,使用方法 .get(key)通过键 key 访问相应的值。

js
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() 创建映射时,可以传入一个 带有键值对的数组(或其它可迭代对象) 来进行初始化

js
// 键值对 [key, value] 数组
let map = new Map([
  ['1',  'str1'],
  [1,    'num1'],
  [true, 'bool1']
]);
alert( map.get('1') ); // str1
Tip

与一般对象最大的差别是 Map 的键 key 可以任何类型的数据,而对象的键只能是字符串。使用对象作为键是 Map 最值得注意和重要的功能之一。

js
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
js
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 本身,所以我们可以进行「链式」调用为映射连续添加多个元素

js
let map = new Map();
map.set('1', 'str1')
  .set(1, 'num1')
  .set(true, 'bool1');

检查键

使用方法 .has(key) 向其传入一个键名,检查 Map 中是否存在该键值对,返回 truefalse

迭代

可以通过三种方式循环访问映射的键、值、键值对

  • 方法 .keys() 返回一个映射的键迭代器,结合 for-of 循环结构遍历所有键
  • 方法 .values()返回一个映射的值迭代器,结构 for-of 循环结构遍历所有值
  • 方法 .entries() 返回一个实体/键值对组成的数组的迭代器,结合 for-of 结构使用(映射自身就是一个可迭代对象,在循环结构 for-of 中默认返回的迭代器就是键值对迭代器),💡 可以使用数组解构来提取相应的数据
js
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() 手动一步一步循环访问每个数据

js
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) 转换为一个数组,以使用数组的特有方法。

js
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() 初始化所需的格式(即元素是由键值对组成的),通过转变对象就可以用来创建映射。

js
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) 即可生成相应的普通对象

js
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 让代码更简洁

js
let obj = Object.fromEntries(map); // 省掉 .entries()

弱映射

根据 垃圾回收 规则可知 JavaScript 引擎在值可访问(并可能被使用)时将其存储在内存中,并不会将空间进行回收,⚠️ 通常当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是可以访问的(即使相应的变量/主存储器已经被覆盖),这会造成回收清理很麻烦。

js
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 弱映射的数据结构代替,以解决垃圾回收的问题。

js
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆盖引用
// john 被存储在 map 中,
// 仍可以使用 map.keys() 来获取它

WeakMapMap 很像,最根本的不同是它不会阻止垃圾回收机制对作为键的对象 key object 的回收以释放内存空间

js
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 弱映射中,那么当该对象被垃圾回收机制回收后,与对象相关的数据也会被自动清除。

js
// 记录用户访问次数
// 以用户对象作为键,其访问次数为值。当一个用户离开时这时就不再需要他的访问次数了(该用户对象将被垃圾回收机制回收)
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 里的键所对应的信息一同被从内存中删除

另外一个普遍的例子是缓存:当一个函数的结果需要被记住/缓存,这样在后续的对同一个对象的调用时,就可以重用这个被缓存的结果;而如果主存储器被重置(对象引用都被删除以后),存储在弱映射中以该对象作为键的数据就会被删除

js
// 📁 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 的键,那么它将被自动清除。


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes