首页 > 编程语言 >JavaScript 编程语言【数据类型】映射|集合|WeakMap and WeakSet

JavaScript 编程语言【数据类型】映射|集合|WeakMap and WeakSet

时间:2024-06-13 18:31:55浏览次数:23  
标签:map set Map 对象 JavaScript 数据类型 WeakMap let john

文章目录

  • Map and Set(映射和集合)
    • Map
    • Map 迭代
    • Object.entries:从对象创建 Map]
    • Object.fromEntries:从 Map 创建对象
    • Set
    • Set 迭代(iteration)
    • 总结
    • ✅任务
      • 过滤数组中的唯一元素
      • 过滤字谜(anagrams)
      • 迭代键
  • WeakMap and WeakSet(弱映射和弱集合)
    • WeakMap
    • 使用案例:额外的数据
    • 使用案例:缓存
    • WeakSet
    • 总结
    • ✅任务
      • 存储 "unread" 标识
      • 保存阅读日期

Map and Set(映射和集合)

学到现在,我们已经了解了以下复杂的数据结构:

  • 对象,存储带有键的数据的集合。
  • 数组,存储有序集合。

但这还不足以应对现实情况。这就是为什么存在 MapSet

Map

[Map](Map(将键映射到值的对象)_百度百科 (baidu.com)) 是一个带键的数据项的集合,就像一个 Object 一样。 但是它们最大的差别是 Map 允许任何类型的键(key)。

它的方法和属性如下:

  • new Map() —— 创建 map。
  • map.set(key, value) —— 根据键存储值。
  • map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
  • map.has(key) —— 如果 key 存在则返回 true,否则返回 false
  • map.delete(key) —— 删除指定键的值。
  • map.clear() —— 清空 map。
  • map.size —— 返回当前元素个数。

举个例子:

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

如我们所见,与对象不同,键不会被转换成字符串。键可以是任何类型。

ℹ️**map[key] 不是使用 Map 的正确方式**

虽然 map[key] 也有效,例如我们可以设置 map[key] = 2,这样会将 map 视为 JavaScript 的 plain object,因此它暗含了所有相应的限制(仅支持 string/symbol 键等)。

所以我们应该使用 map 方法:setget 等。

Map 还可以使用对象作为键。

例如:

let john = { name: "John" };

// 存储每个用户的来访次数
let visitsCountMap = new Map();

// john 是 Map 中的键
visitsCountMap.set(john, 123);

alert( visitsCountMap.get(john) ); // 123

使用对象作为键是 Map 最值得注意和重要的功能之一。在 Object 中,我们则无法使用对象作为键。在 Object 中使用字符串作为键是可以的,但我们无法使用另一个 Object 作为 Object 中的键。

我们来尝试一下:

let john = { name: "John" };
let ben = { name: "Ben" };

let visitsCountObj = {}; // 尝试使用对象

visitsCountObj[ben] = 234; // 尝试将对象 ben 用作键
visitsCountObj[john] = 123; // 尝试将对象 john 用作键,但我们会发现使用对象 ben 作为键存下的值会被替换掉

// 变成这样了!
alert( visitsCountObj["[object Object]"] ); // 123

因为 visitsCountObj 是一个对象,它会将所有 Object 键例如上面的 johnben 转换为字符串 "[object Object]"。这显然不是我们想要的结果。

ℹ️**Map 是怎么比较键的?**

Map 使用 SameValueZero 算法来比较键是否相等。它和严格等于 === 差不多,但区别是 NaN 被看成是等于 NaN。所以 NaN 也可以被用作键。

这个算法不能被改变或者自定义。

ℹ️链式调用

每一次 map.set 调用都会返回 map 本身,所以我们可以进行“链式”调用:

map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');

Map 迭代

如果要在 map 里使用循环,可以使用以下三个方法:

  • map.keys() —— 遍历并返回一个包含所有键的可迭代对象,
  • map.values() —— 遍历并返回一个包含所有值的可迭代对象,
  • map.entries() —— 遍历并返回一个包含所有实体 [key, value] 的可迭代对象,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)
}

ℹ️使用插入顺序

迭代的顺序与插入值的顺序相同。与普通的 Object 不同,Map 保留了此顺序。

除此之外,Map 有内建的 forEach 方法,与 Array 类似:

// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach( (value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Object.entries:从对象创建 Map]

当创建一个 Map 后,我们可以传入一个带有键值对的数组(或其它可迭代对象)来进行初始化,如下所示:

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

alert( map.get('1') ); // str1

如果我们想从一个已有的普通对象(plain object)来创建一个 Map,那么我们可以使用内建方法 Object.entries(obj),该方法返回对象的键/值对数组,该数组格式完全按照 Map 所需的格式。

所以可以像下面这样从一个对象创建一个 Map:

let obj = {
  name: "John",
  age: 30
};

let map = new Map(Object.entries(obj));

alert( map.get('name') ); // John

这里,Object.entries 返回键/值对数组:[ ["name","John"], ["age", 30] ]。这就是 Map 所需要的格式。

Object.fromEntries:从 Map 创建对象

我们刚刚已经学习了如何使用 Object.entries(obj) 从普通对象(plain object)创建 Map

Object.fromEntries 方法的作用是相反的:给定一个具有 [key, value] 键值对的数组,它会根据给定数组创建一个对象:

let prices = Object.fromEntries([
  ['banana', 1],
  ['orange', 2],
  ['meat', 4]
]);

// 现在 prices = { banana: 1, orange: 2, meat: 4 }

alert(prices.orange); // 2

我们可以使用 Object.fromEntriesMap 得到一个普通对象(plain object)。

例如,我们在 Map 中存储了一些数据,但是我们需要把这些数据传给需要普通对象(plain object)的第三方代码。

我们来开始:

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

调用 map.entries() 将返回一个可迭代的键/值对,这刚好是 Object.fromEntries 所需要的格式。

我们可以把带 (*) 这一行写得更短:

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

上面的代码作用也是一样的,因为 Object.fromEntries 期望得到一个可迭代对象作为参数,而不一定是数组。并且 map 的标准迭代会返回跟 map.entries() 一样的键/值对。因此,我们可以获得一个普通对象(plain object),其键/值对与 map 相同。

Set

Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。

它的主要方法如下:

  • new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
  • set.add(value) —— 添加一个值,返回 set 本身
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 返回元素个数。

它的主要特点是,重复使用同一个值调用 set.add(value) 并不会发生什么改变。这就是 Set 里面的每一个值只出现一次的原因。

例如,我们有客人来访,我们想记住他们每一个人。但是已经来访过的客人再次来访,不应造成重复记录。每个访客必须只被“计数”一次。

Set 可以帮助我们解决这个问题:

let set = new Set();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// visits,一些访客来访好几次
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set 只保留不重复的值
alert( set.size ); // 3

for (let user of set) {
  alert(user.name); // John(然后 Pete 和 Mary)
}

Set 的替代方法可以是一个用户数组,用 arr.find 在每次插入值时检查是否重复。但是这样性能会很差,因为这个方法会遍历整个数组来检查每个元素。Set 内部对唯一性检查进行了更好的优化。

Set 迭代(iteration)

我们可以使用 for..offorEach 来遍历 Set:

let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) alert(value);

// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

注意一件有趣的事儿。forEach 的回调函数有三个参数:一个 value,然后是 同一个值 valueAgain,最后是目标对象。没错,同一个值在参数里出现了两次。

forEach 的回调函数有三个参数,是为了与 Map 兼容。当然,这看起来确实有些奇怪。但是这对在特定情况下轻松地用 Set 代替 Map 很有帮助,反之亦然。

Map 中用于迭代的方法在 Set 中也同样支持:

  • set.keys() —— 遍历并返回一个包含所有值的可迭代对象,
  • set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map
  • set.entries() —— 遍历并返回一个包含所有的实体 [value, value] 的可迭代对象,它的存在也是为了兼容 Map

总结

Map —— 是一个带键的数据项的集合。

方法和属性如下:

  • new Map([iterable]) —— 创建 map,可选择带有 [key,value] 对的 iterable(例如数组)来进行初始化。
  • map.set(key, value) —— 根据键存储值,返回 map 自身。
  • map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
  • map.has(key) —— 如果 key 存在则返回 true,否则返回 false
  • map.delete(key) —— 删除指定键对应的值,如果在调用时 key 存在,则返回 true,否则返回 false
  • map.clear() —— 清空 map 。
  • map.size —— 返回当前元素个数。

与普通对象 Object 的不同点:

  • 任何键、对象都可以作为键。
  • 有其他的便捷方法,如 size 属性。

Set —— 是一组唯一值的集合。

方法和属性:

  • new Set([iterable]) —— 创建 set,可选择带有 iterable(例如数组)来进行初始化。
  • set.add(value) —— 添加一个值(如果 value 存在则不做任何修改),返回 set 本身。
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 元素的个数。

MapSet 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。

✅任务

过滤数组中的唯一元素

重要程度5️⃣

定义 arr 为一个数组。

创建一个函数 unique(arr),该函数返回一个由 arr 中所有唯一元素所组成的数组。

例如:

function unique(arr) {
  /* 你的代码 */
}

let values = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(values) ); // Hare, Krishna, :-O

P.S. 这里用到了 string 类型,但其实可以是任何类型的值。

P.S. 使用 Set 来存储唯一值。

打开带有测试的沙箱。

解决方案

function unique(arr) {
return Array.from(new Set(arr));
}

使用沙箱的测试功能打开解决方案。

过滤字谜(anagrams)

重要程度4️⃣

Anagrams 是具有相同数量相同字母但是顺序不同的单词。

例如:

nap - pan
ear - are - era
cheaters - hectares - teachers

写一个函数 aclean(arr),它返回被清除了字谜(anagrams)的数组。

例如:

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) ); // "nap,teachers,ear" or "PAN,cheaters,era"

对于所有的字谜(anagram)组,都应该保留其中一个词,但保留的具体是哪一个并不重要。

打开带有测试的沙箱。

解决方案

为了找到所有字谜(anagram),让我们把每个单词打散为字母并进行排序。当字母被排序后,所有的字谜就都一样了。

例如:

nap, pan -> anp
ear, era, are -> aer
cheaters, hectares, teachers -> aceehrst
...

我们将使用进行字母排序后的单词的变体(variant)作为 map 的键,每个键仅对应存储一个值:

function aclean(arr) {
let map = new Map();

for (let word of arr) {
 // 将单词 split 成字母,对字母进行排序,之后再 join 回来
 let sorted = word.toLowerCase().split('').sort().join(''); // (*)
 map.set(sorted, word);
}

return Array.from(map.values());
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

字母排序在 (*) 行以链式调用的方式完成。

为了方便,我们把它分解为多行:

let sorted = word // PAN
.toLowerCase() // pan
.split('') // ['p','a','n']
.sort() // ['a','n','p']
.join(''); // anp

两个不同的单词 'PAN''nap' 得到了同样的字母排序形式 'anp'

下一行是将单词放入 map:

map.set(sorted, word);

如果我们再次遇到相同字母排序形式的单词,那么它将会覆盖 map 中有相同键的前一个值。因此,每个字母形式(译注:排序后的)最多只有一个单词。(译注:并且是每个字母形式中最靠后的那个值)

最后,Array.from(map.values()) 将 map 的值迭代(我们不需要结果的键)为数组形式,并返回这个数组。

在这里,我们也可以使用普通对象(plain object)而不用 Map,因为键就是字符串。

下面是解决方案:

function aclean(arr) {
let obj = {};

for (let i = 0; i < arr.length; i++) {
 let sorted = arr[i].toLowerCase().split("").sort().join("");
 obj[sorted] = arr[i];
}

return Object.values(obj);
}

let arr = ["nap", "teachers", "cheaters", "PAN", "ear", "era", "hectares"];

alert( aclean(arr) );

使用沙箱的测试功能打开解决方案。

迭代键

重要程度5️⃣

我们期望使用 map.keys() 得到一个数组,然后使用例如 .push 等特定的方法对其进行处理。

但是运行不了:

let map = new Map();

map.set("name", "John");

let keys = map.keys();

// Error: keys.push is not a function
keys.push("more");

为什么?我们应该如何修改代码让 keys.push 工作?

解决方案

这是因为 map.keys() 返回的是可迭代对象而非数组。

我们可以使用方法 Array.from 来将它转换为数组:

let map = new Map();

map.set("name", "John");

let keys = Array.from(map.keys());

keys.push("more");

alert(keys); // name, more

WeakMap and WeakSet(弱映射和弱集合)

我们从前面的 【垃圾回收】章节中知道,JavaScript 引擎在值“可达”和可能被使用时会将其保持在内存中。

例如:

let john = { name: "John" };

// 该对象能被访问,john 是它的引用

// 覆盖引用
john = null;

// 该对象将会被从内存中清除

通常,当对象、数组之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都被认为是可达的。

例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。

就像这样:

let john = { name: "John" };

let array = [ john ];

john = null; // 覆盖引用

// 前面由 john 所引用的那个对象被存储在了 array 中
// 所以它不会被垃圾回收机制回收
// 我们可以通过 array[0] 获取到它

类似的,如果我们使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在。它会占用内存,并且应该不会被(垃圾回收机制)回收。

例如:

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 覆盖引用

// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它

WeakMap 在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收。

让我们通过例子来看看这指的到底是什么。

WeakMap

WeakMapMap 的第一个不同点就是,WeakMap 的键必须是对象,不能是原始值:

let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // 正常工作(以对象作为键)

// 不能使用字符串作为键
weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象

现在,如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。

let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 覆盖引用

// john 被从内存中删除了!

与上面常规的 Map 的例子相比,现在如果 john 仅仅是作为 WeakMap 的键而存在 —— 它将会被从 map(和内存)中自动删除。

WeakMap 不支持迭代以及 keys()values()entries() 方法。所以没有办法获取 WeakMap 的所有键或值。

WeakMap 只有以下的方法:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

为什么会有这种限制呢?这是技术的原因。如果一个对象丢失了其它所有引用(就像上面示例中的 john),那么它就会被垃圾回收机制自动回收。但是在从技术的角度并不能准确知道 何时会被回收

这些都是由 JavaScript 引擎决定的。JavaScript 引擎可能会选择立即执行内存清理,如果现在正在发生很多删除操作,那么 JavaScript 引擎可能就会选择等一等,稍后再进行内存清理。因此,从技术上讲,WeakMap 的当前元素的数量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分。因此,暂不支持访问 WeakMap 的所有键/值的方法。

那么,在哪里我们会需要这样的数据结构呢?

使用案例:额外的数据

WeakMap 的主要应用场景是 额外数据的存储

假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候 WeakMap 正是我们所需要的利器。

我们将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。

weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 将会被自动清除

让我们来看一个例子。

例如,我们有用于处理用户访问计数的代码。收集到的信息被存储在 map 中:一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了。

下面是一个使用 Map 的计数函数的例子:

// 

标签:map,set,Map,对象,JavaScript,数据类型,WeakMap,let,john
From: https://blog.csdn.net/weixin_51568389/article/details/126448427

相关文章

  • 一起来学javascript-axios
       <!--//AJAX的封装插件——Axios。  //什么是Axios  //Axios是一个基于Promise的HTTP库,可以用于浏览器和Node.js,支持VanillaJS、Angular、React、Vue等框架。  //简单的理解就是对Ajax的封装,且具有易用、简洁、高效等特点。  ......
  • 使用 JavaScript 中的 DeviceOrientationEvent
    在前端开发中,DeviceOrientationEvent是一个非常有用的API,它允许我们访问设备的物理方向信息,如设备的倾斜和旋转。这个API可以在移动设备上获取设备的方向,可以用来创建各种有趣和交互性强的应用程序,比如游戏、增强现实体验等。本文将介绍如何使用DeviceOrientationEventAP......
  • JavaScript-DOM
    DOM全称:DOM(DocumentObjectModel--文档对象类型) 作用:用来操控网页类容的功能,开发网页特效和实现用户交互DOM结构将HTML文档以树形结构表现出来称之为DOM树获取DOM 语法:document.querySelector('css选择器')参数:包含一个或多个css选择器字符串返回值:CSS选......
  • web网页设计期末课程大作业:家乡旅游主题网站设计——河北8页html css javascript
    ......
  • 【保姆级详细介绍JavaScript初识及基本语法】
    ......
  • JavaScript获取表单数据转换时间格式(学习自用)
    JavaScript获取表单数据转换时间格式:getTime(date){letdateNow=newDate(date);functionconvert(data){//时间为个位数时,前面加个0,使观感更好returndata<10?"0"+data:data;}letyear......
  • 二级web基础操作题练习(6)——嵌入Javascript代码
    -----要求----- 用JavaScript编写如图1所示页面,当在第一个文本框中输入URLhttp://test.cn/index.jsp?key0=0&key1=1&key2=2,点击按钮“解析参数“后,在下面的三个文本框中分别显示解析出来的参数,如图2所示。图1图2 -----代码示例-----<html><head><!--设置字符编......
  • 数据类型与深浅拷贝理解 vs 不同语言环境下深浅拷贝实例
    一、在探讨深浅拷贝的问题之前需要先理解两种数据类型值类型数据(基本数据类型):存储在栈内存,占据固定大小的空间,直接存储其数据(值的赋值是深拷贝);引用类型数据:这种数据类型的变量通常存储在栈区,存的是指向实际数据的指针,而实际的数据是存储在堆区,访问数据通过指针去访问(值的赋值......
  • JavaScript常用的流程控制语句
    在JavaScript中,有以下几种常用的流程控制语句:if...else:当if条件为假时,可以使用else语句执行另一段代码。if(condition){//条件为真时执行的代码}else{//条件为假时执行的代码}if...elseif...else:可以链式使用多个if和elseif来根据多个条件执行不......
  • JavaScript判断数据为对象(转载)
    1.javaScript判断数据为对象1.1. Object.prototype.toString.call()2.JavaScript如何判断数据类型2.1. typeof2.2. Array.isArray()2.3. instanceof2.4. Object.prototype.toString.call()2.5. constructor2.6. 使用ES6的Symbol.toStringTa......