JavaScript 编程语言【数据类型】映射|集合|WeakMap and WeakSet
@[toc]
Map and Set(映射和集合)
学到现在,我们已经了解了以下复杂的数据结构:
- 对象,存储带有键的数据的集合。
- 数组,存储有序集合。
但这还不足以应对现实情况。这就是为什么存在 Map
和 Set
。
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
方法:set
和get
等。
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
键例如上面的 john
和 ben
转换为字符串 "[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.fromEntries
从 Map
得到一个普通对象(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..of
或 forEach
来遍历 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
—— 元素的个数。
在 Map
和 Set
中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。
✅任务
过滤数组中的唯一元素
重要程度:five:
定义 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)
重要程度:four:
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) );
迭代键
重要程度:five:
我们期望使用 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
WeakMap
和 Map
的第一个不同点就是,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
的计数函数的例子:
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
// 递增用户来访次数
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
下面是其他部分的代码,可能是使用它的其它代码:
// 📁 main.js
let john = { name: "John" };
countUser(john); // count his visits
// 不久之后,john 离开了
john = null;
现在,john
这个对象应该被垃圾回收,但它仍在内存中,因为它是 visitsCountMap
中的一个键。
当我们移除用户时,我们需要清理 visitsCountMap
,否则它将在内存中无限增大。在复杂的架构中,这种清理会成为一项繁重的任务。
我们可以通过使用 WeakMap
来避免这样的问题:
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// 递增用户来访次数
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
现在我们不需要去清理 visitsCountMap
了。当 john
对象变成不可达时,即便它是 WeakMap
里的一个键,它也会连同它作为 WeakMap
里的键所对应的信息一同被从内存中删除。
使用案例:缓存
另外一个常见的例子是缓存。我们可以存储(“缓存”)函数的结果,以便将来对同一个对象的调用可以重用这个结果。
为了实现这一点,我们可以使用 Map
(非最佳方案):
// 📁 cache.js
let cache = new Map();
// 计算并记住结果
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculations of the result for */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
// 现在我们在其它文件中使用 process()
// 📁 main.js
let obj = {/* 假设我们有个对象 */};
let result1 = process(obj); // 计算完成
// ……稍后,来自代码的另外一个地方……
let result2 = process(obj); // 取自缓存的被记忆的结果
// ……稍后,我们不再需要这个对象时:
obj = null;
alert(cache.size); // 1(啊!该对象依然在 cache 中,并占据着内存!)
对于多次调用同一个对象,它只需在第一次调用时计算出结果,之后的调用可以直接从 cache
中获取。这样做的缺点是,当我们不再需要这个对象的时候需要清理 cache
。
如果我们用 WeakMap
替代 Map
,便不会存在这个问题。当对象被垃圾回收时,对应缓存的结果也会被自动从内存中清除。
// 📁 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);
}
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// ……稍后,我们不再需要这个对象时:
obj = null;
// 无法获取 cache.size,因为它是一个 WeakMap,
// 要么是 0,或即将变为 0
// 当 obj 被垃圾回收,缓存的数据也会被清除
WeakSet
WeakSet
的表现类似:
- 与
Set
类似,但是我们只能向WeakSet
添加对象(而不能是原始值)。 - 对象只有在其它某个(些)地方能被访问的时候,才能留在
WeakSet
中。 - 跟
Set
一样,WeakSet
支持add
,has
和delete
方法,但不支持size
和keys()
,并且不可迭代。
变“弱(weak)”的同时,它也可以作为额外的存储空间。但并非针对任意数据,而是针对“是/否”的事实。WeakSet
的元素可能代表着有关该对象的某些信息。
例如,我们可以将用户添加到 WeakSet
中,以追踪访问过我们网站的用户:
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John 访问了我们
visitedSet.add(pete); // 然后是 Pete
visitedSet.add(john); // John 再次访问
// visitedSet 现在有两个用户了
// 检查 John 是否来访过?
alert(visitedSet.has(john)); // true
// 检查 Mary 是否来访过?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet 将被自动清理(即自动清除其中已失效的值 john)
WeakMap
和 WeakSet
最明显的局限性就是不能迭代,并且无法获取所有当前内容。那样可能会造成不便,但是并不会阻止 WeakMap/WeakSet
完成其主要工作 —— 成为在其它地方管理/存储“额外”的对象数据。
总结
WeakMap
是类似于 Map
的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问这些对象,垃圾回收便会将这些对象与其关联值一同删除。
WeakSet
是类似于 Set
的集合,它仅存储对象,并且一旦通过其他方式无法访问这些对象,垃圾回收便会将这些对象删除。
它们的主要优点是它们对对象是弱引用,所以被它们引用的对象很容易地被垃圾收集器移除。
这是以不支持 clear
、size
、keys
、values
等作为代价换来的……
WeakMap
和 WeakSet
被用作“主要”对象存储之外的“辅助”数据结构。一旦将对象从主存储器中删除,如果该对象仅被用作 WeakMap
或 WeakSet
的键,那么该对象将被自动清除。
✅任务
存储 “unread” 标识
重要程度:five:
这里有一个 messages 数组:
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
你的代码可以访问它,但是 message 是由其他人的代码管理的。该代码会定期添加新消息,删除旧消息,但是你不知道这些操作确切的发生时间。
现在,你应该使用什么数据结构来保存关于消息“是否已读”的信息?该结构必须很适合对给定的 message 对象给出“它读了吗?”的答案。
P.S. 当一个消息被从 messages
中删除后,它应该也从你的数据结构中消失。
P.S. 我们不能修改 message 对象,例如向其添加我们的属性。因为它们是由其他人的代码管理的,我们修改该数据可能会导致不好的后果。
解决方案
让我们将已读消息存储在
WeakSet
中:let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; let readMessages = new WeakSet(); // 两个消息已读 readMessages.add(messages[0]); readMessages.add(messages[1]); // readMessages 包含两个元素 // ……让我们再读一遍第一条消息! readMessages.add(messages[0]); // readMessages 仍然有两个不重复的元素 // 回答:message[0] 已读? alert("Read message 0: " + readMessages.has(messages[0])); // true messages.shift(); // 现在 readMessages 有一个元素(技术上来讲,内存可能稍后才会被清理)
WeakSet
允许存储一系列的消息,并且很容易就能检查它是否包含某个消息。它会自动清理自身。代价是,我们不能对它进行迭代,也不能直接从中获取“所有已读消息”。但是,我们可以通过遍历所有消息,然后找出存在于 set 的那些消息来完成这个功能。
另一种不同的解决方案可以是,在读取消息后向消息添加诸如
message.isRead=true
之类的属性。由于messages
对象是由另一个代码管理的,因此通常不建议这样做,但是我们可以使用 symbol 属性来避免冲突。像这样:
// symbol 属性仅对于我们的代码是已知的 let isRead = Symbol("isRead"); messages[0][isRead] = true;
现在,第三方代码可能看不到我们的额外属性。
尽管 symbol 可以降低出现问题的可能性,但从架构的角度来看,还是使用
WeakSet
更好。
保存阅读日期
重要程度:five:
这儿有一个和 【上一个任务】类似的 messages
数组。场景也相似。
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
现在的问题是:你建议采用什么数据结构来保存信息:“消息是什么时候被阅读的?”。
在前一个任务中我们只需要保存“是/否”。现在我们需要保存日期,并且它应该在消息被垃圾回收时也被从内存中清除。
P.S. 日期可以存储为内建的 Date
类的对象,稍后我们将进行介绍。
解决方案
我们可以使用
WeakMap
保存日期:let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ]; let readMap = new WeakMap(); readMap.set(messages[0], new Date(2017, 1, 1)); // 我们稍后将学习 Date 对象
- 点赞
- 收藏
- 关注作者
评论(0)