文档
Map
概述
- 一个带键的数据项的集合,就像一个
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
—— 返回当前元素个数
1 2 3 4 5 6 7 8 9 10 11 12 |
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
最值得注意和重要的功能之一
1 2 3 4 5 6 7 8 9 |
let john = { name: "John" }; // 存储每个用户的来访次数 let visitsCountMap = new Map(); // john 是 Map 中的键 visitsCountMap.set(john, 123); alert( visitsCountMap.get(john) ); // 123 |
- 在
Object
中使用字符串作为键是可以的,但我们无法使用另一个Object
作为Object
中的键- 因为
visitsCountObj
是一个对象,它会将所有Object
键例如上面的john
和ben
转换为字符串"[object Object]"
- 因为
1 2 3 4 5 6 7 8 9 10 |
let john = { name: "John" }; let ben = { name: "Ben" }; let visitsCountObj = {}; // 尝试使用对象 visitsCountObj[ben] = 234; // 尝试将对象 ben 用作键 visitsCountObj[john] = 123; // 尝试将对象 john 用作键,但我们会发现使用对象 ben 作为键存下的值会被替换掉 // 变成这样了! alert( visitsCountObj["[object Object]"] ); // 123 |
Map
是怎么比较键的?
Map
使用 SameValueZero 算法来比较键是否相等- 它和严格等于
===
差不多,但区别是NaN
被看成是等于NaN
。所以NaN
也可以被用作键
链式调用
- 每一次
map.set
调用都会返回map
本身,所以我们可以进行“链式”调用:
1 2 3 |
map.set('1', 'str1') .set(1, 'num1') .set(true, 'bool1'); |
Map
迭代
- 如果要在
map
里使用循环,可以使用以下三个方法:map.keys()
—— 遍历并返回一个包含所有键的可迭代对象,map.values()
—— 遍历并返回一个包含所有值的可迭代对象,map.entries()
—— 遍历并返回一个包含所有实体[key, value]
的可迭代对象,for..of
在默认情况下使用的就是这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
类似:
1 2 3 4 |
// 对每个键值对 (key, value) 运行 forEach 函数 recipeMap.forEach( (value, key, map) => { alert(`${key}: ${value}`); // cucumber: 500 etc }); |
Object.entries
:从对象创建 Map
- 当创建一个
Map
后,我们可以传入一个带有键值对的数组(或其它可迭代对象)来进行初始化,如下所示:
1 2 3 4 5 6 7 8 |
// 键值对 [key, value] 数组 let map = new Map([ ['1', 'str1'], [1, 'num1'], [true, 'bool1'] ]); alert( map.get('1') ); // str1 |
- 如果我们想从一个已有的普通对象(
plain object
)来创建一个Map
,那么我们可以使用内建方法Object.entries(obj)
- 该方法返回对象的键/值对数组,该数组格式完全按照
Map
所需的格式 - 这里,
Object.entries
返回键/值对数组:[ ["name","John"], ["age", 30] ]
- 该方法返回对象的键/值对数组,该数组格式完全按照
1 2 3 4 5 6 7 8 |
let obj = { name: "John", age: 30 }; let map = new Map(Object.entries(obj)); alert( map.get('name') ); // John |
Object.fromEntries
:从 Map
创建对象
Object.fromEntries
方法的作用是相反的:给定一个具有[key, value]
键值对的数组,它会根据给定数组创建一个对象:
1 2 3 4 5 6 7 8 9 |
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
)的第三方代码 - 调用
map.entries()
将返回一个可迭代的键/值对,这刚好是Object.fromEntries
所需要的格式
- 例如,我们在
1 2 3 4 5 6 7 8 9 10 11 |
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 |
- 这个代码作用也是一样的
- 因为
Object.fromEntries
期望得到一个可迭代对象作为参数,而不一定是数组 - 并且
map
的标准迭代会返回跟map.entries()
一样的键/值对 - 因此,我们可以获得一个普通对象(
plain object
),其键/值对与map
相同
- 因为
1 |
let obj = Object.fromEntries(map); // 省掉 .entries() |
Set
概述
- 一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次
- 它的主要特点是,重复使用同一个值调用
set.add(value)
并不会发生什么改变
- 它的主要特点是,重复使用同一个值调用
方法
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
—— 返回元素个数
示例
- 例如,我们有客人来访,我们想记住他们每一个人
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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
迭代
- 可以使用
for..of
或forEach
来遍历Set
:
1 2 3 4 5 6 7 8 |
let set = new Set(["oranges", "apples", "bananas"]); for (let value of set) alert(value); // 与 forEach 相同: set.forEach((value, valueAgain, set) => { alert(value); }); |
Map
中用于迭代的方法在Set
中也同样支持:set.keys()
—— 遍历并返回一个包含所有值的可迭代对象,set.values()
—— 与set.keys()
作用相同,这是为了兼容Map
,set.entries()
—— 遍历并返回一个包含所有的实体[value, value]
的可迭代对象,它的存在也是为了兼容Map
注意
forEach
的回调函数有三个参数:一个value
,然后是 同一个值valueAgain
,最后是目标对象forEach
的回调函数有三个参数,是为了与Map
兼容
WeakMap
概述
- 从前面的 垃圾回收章节中知道,
JavaScript
引擎在值“可达”和可能被使用时会将其保持在内存中
1 2 3 4 5 6 7 8 |
let john = { name: "John" }; // 该对象能被访问,john 是它的引用 // 覆盖引用 john = null; // 该对象将会被从内存中清除 |
- 通常,当对象、数组之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都被认为是可达的
- 例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用
1 2 3 4 5 6 7 8 9 |
let john = { name: "John" }; let array = [ john ]; john = null; // 覆盖引用 // 前面由 john 所引用的那个对象被存储在了 array 中 // 所以它不会被垃圾回收机制回收 // 我们可以通过 array[0] 获取到它 |
- 类似的,如果我们使用对象作为常规
Map
的键,那么当Map
存在时,该对象也将存在。它会占用内存,并且不会被(垃圾回收机制)回收
1 2 3 4 5 6 7 8 9 |
let john = { name: "John" }; let map = new Map(); map.set(john, "..."); john = null; // 覆盖引用 // john 被存储在了 map 中, // 我们可以使用 map.keys() 来获取它 |
WeakMap
在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object
)的回收
键必须是对象
WeakMap
和Map
的第一个不同点就是,WeakMap
的键必须是对象,不能是原始值:
1 2 3 4 5 6 7 8 |
let weakMap = new WeakMap(); let obj = {}; weakMap.set(obj, "ok"); // 正常工作(以对象作为键) // 不能使用字符串作为键 weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象 |
- 现在,如果我们在
weakMap
中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map
)中自动清除
1 2 3 4 5 6 7 8 |
let john = { name: "John" }; let weakMap = new WeakMap(); weakMap.set(john, "..."); john = null; // 覆盖引用 // john 被从内存中删除了! |
方法
WeakMap
不支持迭代以及keys()
,values()
和entries()
方法- 所以没有办法获取
WeakMap
的所有键或值 - 为什么会有这种限制呢?
- 这是技术的原因
如果一个对象丢失了其它所有引用(就像上面示例中的john
),那么它就会被垃圾回收机制自动回收
但是在从技术的角度并不能准确知道 何时会被回收 - 这些都是由
JavaScript
引擎决定的
JavaScript
引擎可能会选择立即执行内存清理,如果现在正在发生很多删除操作,那么JavaScript
引擎可能就会选择等一等,稍后再进行内存清理 - 因此,从技术上讲,
WeakMap
的当前元素的数量是未知的
JavaScript
引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分
- 所以没有办法获取
WeakMap
只有以下的方法:weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
使用案例:额外数据的存储
WeakMap
的主要应用场景是 额外数据的存储- 假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候
WeakMap
正是我们所需要的利器 - 我们将这些数据放到
WeakMap
中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除
- 假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候
1 2 |
weakMap.set(john, "secret documents"); // 如果 john 消失,secret documents 将会被自动清除 |
- 示例
2
- 例如,我们有用于处理用户访问计数的代码。收集到的信息被存储在
map
中:一个用户对象作为键,其访问次数为值 - 当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了
- 例如,我们有用于处理用户访问计数的代码。收集到的信息被存储在
1 2 3 4 5 6 7 8 |
// 📁 visitsCount.js let visitsCountMap = new Map(); // map: user => visits count // 递增用户来访次数 function countUser(user) { let count = visitsCountMap.get(user) || 0; visitsCountMap.set(user, count + 1); } |
1 2 3 4 5 6 7 |
// 📁 main.js let john = { name: "John" }; countUser(john); // count his visits // 不久之后,john 离开了 john = null; |
- 现在,
john
这个对象应该被垃圾回收,但它仍在内存中,因为它是visitsCountMap
中的一个键- 当我们移除用户时,我们需要清理
visitsCountMap
,否则它将在内存中无限增大 - 在复杂的架构中,这种清理会成为一项繁重的任务
- 可以通过使用
WeakMap
来避免这样的问题:
现在我们不需要去清理visitsCountMap
了
当john
对象变成不可达时,即便它是WeakMap
里的一个键,它也会连同它作为WeakMap
里的键所对应的信息一同被从内存中删除
- 当我们移除用户时,我们需要清理
1 2 3 4 5 6 7 8 |
// 📁 visitsCount.js let visitsCountMap = new WeakMap(); // weakmap: user => visits count // 递增用户来访次数 function countUser(user) { let count = visitsCountMap.get(user) || 0; visitsCountMap.set(user, count + 1); } |
使用案例:缓存
- 另外一个常见的例子是缓存
- 我们可以存储(“缓存”)函数的结果,以便将来对同一个对象的调用可以重用这个结果
- 为了实现这一点,我们可以使用
Map
(非最佳方案):- 对于多次调用同一个对象,它只需在第一次调用时计算出结果,之后的调用可以直接从
cache
中获取 - 这样做的缺点是,当我们不再需要这个对象的时候需要清理
cache
- 对于多次调用同一个对象,它只需在第一次调用时计算出结果,之后的调用可以直接从
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 📁 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 中,并占据着内存!) |
- 如果我们用
WeakMap
替代Map
,便不会存在这个问题- 当对象被垃圾回收时,对应缓存的结果也会被自动从内存中清除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 📁 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
中,以追踪访问过我们网站的用户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
完成其主要工作 —— 为在其它地方存储/管理的对象数据提供“额外”存储
Object.keys
,values
,entries
概述
- 这些方法是通用的,有一个共同的约定来将它们用于各种数据结构
- 如果我们创建一个我们自己的数据结构,我们也应该实现这些方法
- 它们支持:
Map
Set
Array
普通对象
- 对于普通对象,下列这些方法是可用的:
Object.keys(obj)
—— 返回一个包含该对象所有的键的数组。Object.values(obj)
—— 返回一个包含该对象所有的值的数组。Object.entries(obj)
—— 返回一个包含该对象所有[key, value]
键值对的数组
- 和
map
的区别- 第一个区别是,对于对象我们使用的调用语法是
Object.keys(obj)
,而不是obj.keys()
- 第二个区别是
Object.*
方法返回的是“真正的”数组对象,而不只是一个可迭代对象
- 第一个区别是,对于对象我们使用的调用语法是
Map | Object | |
调用语法 | map.keys() |
Object.keys(obj) ,而不是 obj.keys() |
返回值 | 可迭代对象 | “真正的”数组 |
1 2 3 4 |
let user = { name: "John", age: 30 }; |
- 对于上面的对象
user
Object.keys(user) = ["name", "age"]
- `
Object.values(user) = ["John", 30]
Object.entries(user) = [ ["name","John"], ["age",30] ]
1 2 3 4 5 6 7 8 9 |
let user = { name: "John", age: 30 }; // 遍历所有的值 for (let value of Object.values(user)) { alert(value); // John, then 30 } |
注意
- 会忽略
symbol
属性- 就像
for..in
循环一样,这些方法会忽略使用Symbol(...)
作为键的属性
- 就像
转换对象
- 对象缺少数组存在的许多方法,例如
map
和filter
等 - 如果我们想应用它们,那么我们可以使用
Object.entries
,然后使用Object.fromEntries
:- 使用
Object.entries(obj)
从obj
获取由键/值对组成的数组 - 对该数组使用数组方法,例如
map
,对这些键/值对进行转换 - 对结果数组使用
Object.fromEntries(array)
方法,将结果转回成对象
- 使用
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let prices = { banana: 1, orange: 2, meat: 4, }; let doublePrices = Object.fromEntries( // 将价格转换为数组,将每个键/值对映射为另一对 // 然后通过 fromEntries 再将结果转换为对象 Object.entries(prices).map(entry => [entry[0], entry[1] * 2]) ); alert(doublePrices.meat); // 8 |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ cef:任务、IPC、网络相关04/30
- ♥ 【Javascript】第二部分05/14
- ♥ 【LeetCode-30 天 JavaScript 挑战】07/23
- ♥ HTTP协议相关学习一03/22
- ♥ Chromium:学习,框架,一09/02
- ♥ 【Javascript】装饰器,转发,call,apply,函数绑定,箭头函数04/06