ES6规范引入了新的数据类型Map和Set
Map 和 Objects 的区别
- 一个 Object 的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值。
- Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
- Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。
- Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
- Map 是 iterable 的,所以可以直接被迭代,而迭代一个Object需要以某种方式获取它的键然后才能迭代。
- 在频繁增删键值对的场景下,Map表现更好。
参考文章
关于可迭代对象
- 可参考可迭代对象 MDN,非常详细
- Javascript中最常见的迭代器是Array,String、Array、TypedArray、Map 和 Set 都是内置可迭代对象,因为它们的原型对象都拥有一个
Symbol.iterator
方法 - Iterator 对象的**next()**:
- 返回具有两个属性的对象: value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true(例子)
Map 键值对二维数组
- Map MDN很全面,可以直接看!
- Map是一组键值对的结构,具有极快的查找速度。
- 任何值(对象或者原始值) 都可以作为一个键或一个值。
- 使用场景:在频繁增删键值对的场景下,Map表现比普通obj更好
- 【创建方法】用Map实现,只需要一个“名字”-“成绩”的对照表,可直接根据名字查找成绩:
1
2
3
4
5let m1 = new Map() ; // 创建方法1
let m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]); // 创建方法2
m.get('Michael'); // 查询 95
m.set('Lily', 100); // 添加
m.delete('Bob'); // 删除 - 初始化Map需要一个二维数组,或者直接初始化一个空Map
- 该二维数组中一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉
键的比较
- NaN 是与 NaN 相等的(虽然
NaN !== NaN
),剩下所有其它的值是根据===
运算符的结果判断是否相等 - 在目前的ECMAScript规范中,**
-0
和+0
被认为是相等的**
Map对象的方法
mapObj.entries()
- entries 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组
- MDN
mapObj.keys()
- keys 方法返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键
- MDN
mapObj.values()
- values 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值
- MDN
mapObj.clear()
- 移除Map对象中的所有元素
- MDN
遍历Map
for…of
1 | var myMap = new Map(); |
forEach()
1 | var myMap = new Map(); |
Map对象的操作
Map 与 Array的转换
1 | let kvArray = [["key1", "value1"], ["key2", "value2"]]; |
Map 的克隆
1 | let original = new Map([ |
Map 的合并
- Map对象间可以进行合并,但是会保持键的唯一性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let first = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let second = new Map([
[1, 'uno'],
[2, 'dos']
]);
// 合并两个Map对象时,如果有重复的键值,则后面的会覆盖前面的。
// 展开运算符本质上是将Map对象转换成数组。
let merged = new Map([...first, ...second]);
console.log(merged.get(1)); // uno
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three - Map对象也能与数组合并:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let first = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let second = new Map([
[1, 'uno'],
[2, 'dos']
]);
// Map对象同数组进行合并时,如果有重复的键值,则后面的会覆盖前面的。
let merged = new Map([...first, ...second, [1, 'eins']]);
console.log(merged.get(1)); // eins
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three
Set 唯一值数据集合
- 可参考MDN
- Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key
- 注意:
- 加入 Set 的值不会发生类型转化,所以1和”1”是两个不同的值
- 在Set内部是通过
===
来判断,即,两个对象永远不可能相同,因为地址不同- 除了NaN,虽然
NaN !== NaN
但是Set可以针对NaN去重
- 除了NaN,虽然
- Set 的三个基本操作:
1
2
3
4
5
6
7// 【创建方法】直接创建一个空Set/提供一个Array作为输入
let s = new Set(); // 创建方法1 空Set
let s = new Set([1, 2, 3]); // 创建方法2 含1, 2, 3
s.add('a'); // 添加
s.has('a'); // 查询
s.delete('a'); // 删除
Set对象的属性 方法
- 可参考MDN,非常直观
- 基本上除了key相关以及增加的方法,其他都和Map的非常相似
- 声明:
const set = new Set()
- size属性:返回 Set 对象中值的个数
- add(): 在Set对象尾部添加一个元素。返回该Set对象
- delete(): 移除Set的中与这个值相等的元素,有则返回true,无则返回false
- clear(): 清楚Set的所有元素
- has(): 是否存在这个值,如果存在为 true,否则为false
- keys():以属性值遍历器的对象
- values():以属性值遍历器的对象
- entries():以属性值和属性值遍历器的对象
- forEach():遍历每个元素
- 声明:
遍历Set
- forEach()
- 虽然Set只能用forEach()遍历,但可以通过转为数组进行更多操作,比如:
1
2let set = new Set([1,2,3])
console.log(new Set([...set].map(v => v * 2))) // Set(3) {2, 4, 6}
使用场景
- 当要进行高频的添加、删除、查询操作时,比起顺序存储的 Array ,使用哈希表的 Set 更加适合,因为它充分利用了哈希函数的特性,具有较低的平均时间复杂度
- 前提是不需要保持顺序,需要保持元素顺序时数组更合适
- Set更快的原因:
- 避免重复元素: Set 数据结构保证了其中不会有重复的元素,这在高频的添加操作场景下是非常有用的,可以快速判断一个元素是否已经存在于 Set 中,避免了重复添加
- 无需保持顺序: 在高频的添加、删除操作场景下,哈希表的 Set 数据结构不需要保持元素的顺序,这使得插入和删除操作更加高效。而数组需要保持元素的顺序,所以在插入和删除操作时可能需要移动其他元素
- 快速的插入和删除操作: 哈希表的插入和删除操作通常具有较快的平均时间复杂度,通常为 O(1)。这是因为哈希表使用哈希函数将元素映射到索引,所以在插入和删除时,通过计算哈希值可以快速定位到元素的位置。而数组在插入和删除操作时可能需要移动其他元素,导致平均复杂度更高
- 高效的查询操作: 哈希表的查询操作也具有较快的平均时间复杂度,通常为 O(1)。通过哈希函数,可以迅速定位到元素的索引,从而实现高效的查询。而在数组中,进行查询操作可能需要线性扫描,导致平均复杂度更高
Set 数组去重
- 注意: Set内部是通过
===
判断的,对引用类型判断依据的是引用地址- 注意NaN,虽然
NaN !== NaN
但是Set可以针对NaN去重
- 注意NaN,虽然
- 重复元素在Set中自动被过滤 (注意 数字3 和 字符串
3
是不同的元素):
1 | const arr = [1, 2, 3, 3, '3'] |
数组相关的面试题
问题:给定一个有序数组,数组里有重复元素,要求原地操作数组,删除重复元素。
答案:
1 | let arr = [1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 11, 22, 11, 22, 33]; |
- 设置len为不重复数组项的index(也就是将不重复数替换到
原数组[len]
位置上,这样就不用新开一个数组了) - 在原数组上循环,i为循环的index
- 原数组中找
arr[i]
比对是否是该数组项的第一个位置,是则将arr[i]
赋给arr[len]
(也就是将不同的这个数放到原数组靠前的位置上),再len++
(使得下一次替换的原数组index+1),不是就不管,i++进行下一次循环 - 全部去重后,使用splice(len)将len后面的多余数据删掉
- 注意,最后一次符合if条件时len+1的位置还没用,所以删除时len最末尾也还属于 重复数组 ,一并删去
求并集,交集和差集
1 | let a = new Set([1, 2, 3]) |
WeakMap和WeakSet
- 重要理解:
- WeakMap成员键、WeakSet成员值只能为对象
- 它们都是”弱引用”集合,WeakMap中键没有其他强引用时(例如将变量赋值为
null
或者离开了作用域,或者整个对象被替换=>引用地址发生变化,对应键值对会被自动删除
- 定义: 同Set、Map的结构,WeakMap成员键、WeakSet成员值只能为对象
- 声明:
const set = new WeakSet()
、const set = new WeakMap()
- WeakMap方法:
- set(): 添加键值对,返回实例
- get(): 返回键值对
- delete(): 删除键值对,如果存在为 true,否则为false
- has(): 是否存在这个值,如果存在为 true,否则为false
- WeakSet方法:
- add(): 在WeakSet对象尾部添加一个元素。返回该实例
- delete(): 移除WeakSet的中与这个值相等的元素
- has(): 判断是否存在这个值,如果存在为 true,否则为false
使用场景
- WeakMap和WeakSet是ES6(ECMAScript 2015)中引入的两种新的数据结构,它们在某些场景下具有独特的优势。它们都是”弱引用”集合,WeakMap中键没有其他强引用时(例如将变量赋值为
null
或者离开了作用域,或者整个对象被替换=>引用地址发生变化),对应键值对会被删除,意味着它们中存储的对象不会阻止垃圾回收器回收这些对象,从而在一些特定情况下非常有用 - WeakMap:
- 弱引用键关联的数据: WeakMap允许您将弱引用的键与相应的值关联起来。这意味着,如果一个对象作为WeakMap的键,而这个对象没有其他强引用,垃圾回收器可以自由地回收这个对象,同时WeakMap中的对应键值对也会被移除
- 防止内存泄漏: 当您需要在不再需要时确保对象被垃圾回收,但又不想手动删除键值对时,WeakMap非常有用。这可以防止由于保留对不再需要的对象的引用而导致的内存泄漏问题。
- 隐私性和安全性: 如果您希望存储的数据不能被外部访问或篡改,WeakMap可以用于存储私有数据,因为WeakMap的键是弱引用,外部无法直接获取键来访问值。
- WeakSet:
- 弱引用的值集合: WeakSet允许您存储一组值,这些值都是弱引用的。与WeakMap类似,如果一个对象作为WeakSet中的值,但没有其他强引用,那么这个对象可以被垃圾回收,同时WeakSet中的该值也会被移除。
- 不能避免重复值: 在一个 WeakSet 中可以多次存储同一个对象的弱引用。WeakSet 并不能自动去重
- 使用场景:
- 缓存: WeakMap可以用于实现一种缓存机制,其中对象被用作键,而值是计算得到的结果。一旦这些对象(键)不再被其他部分强引用/强引用发生变化(例如将变量赋值为
null
或者离开了作用域,或者整个对象被替换=>引用地址发生变化),垃圾回收器可以自由地回收这些对象(键),缓存中的对应键值对也会被自动从WeakMap中移除,从而节省内存(例子见下)- 普通数组是强引用,可以用于缓存,但需要更多的手动管理,包括在不需要时手动从数组中移除缓存对象等
- DOM元素关联数据: 在处理DOM元素时,可以使用WeakMap来存储元素与相关数据之间的关联,当元素被从DOM中移除时,垃圾回收器会自动清理这些数据。
- 事件处理程序: 在为DOM元素添加事件处理程序时,可以使用WeakMap来存储事件处理函数,以避免内存泄漏。
- 防止内存泄漏: 当您需要在不再需要对象时确保对象被垃圾回收,而又不想手动清除相关数据时,WeakMap和WeakSet都是有用的选择。
- 缓存: WeakMap可以用于实现一种缓存机制,其中对象被用作键,而值是计算得到的结果。一旦这些对象(键)不再被其他部分强引用/强引用发生变化(例如将变量赋值为
- 注意: 由于WeakMap和WeakSet的特性,它们并不适合所有场景。如果需要强引用并且确保对象不会被垃圾回收,那么应该使用Map和Set
缓存
当使用 WeakMap 来实现缓存机制时,通常是将某些昂贵的计算结果关联到特定的对象上,并且只要这些对象还有其他的强引用,那么计算结果就会被保留在缓存中。
其中对象被用作键,而值是计算得到的结果。
一旦这些对象(键)不再被其他部分强引用/强引用发生变化(例如将变量赋值为 null
或者离开了作用域,或者整个对象被替换=>引用地址发生变化),垃圾回收器可以自由地回收这些对象(键),从而自动清理缓存。
以下是一个使用 WeakMap 实现缓存机制的示例:
1 | const cache = new WeakMap(); |
补充了解:普通数组是强引用,可以用于缓存,但需要更多的手动管理,包括在不需要时手动从数组中移除对象等
DOM元素关联数据 防止内存泄漏
没有缓存的好理解啊,找了个例子,但没懂绑定handlers是为啥,以后有空回来再看看。
WeakMap
判断元素不再被引用是基于 JavaScript 垃圾回收机制中的弱引用概念。当一个对象不再有强引用指向它时,它会成为垃圾回收的候选对象,可以被垃圾回收器回收。
当使用 WeakMap
关联元素与数据时,在 WeakMap
中使用某个元素作为键,如果您不再有强引用指向这些元素(例如将变量赋值为 null
或者离开了作用域),那么这些元素将成为弱引用,WeakMap
将自动移除该键值对,从而触发与该元素关联的数据的清理,一旦垃圾回收器执行,与这些元素关联的数据也会被清理
1 | const eventHandlers = new WeakMap(); |
要注意的是,垃圾回收的执行时机由 JavaScript 引擎控制,不是立即发生的。这意味着在一个元素不再被引用之后,数据不会立即被清理。垃圾回收会在适当的时间点执行,清理不再有引用的对象和数据。
如果使用普通对象
如果不使用 WeakMap 而是使用普通的对象来关联 DOM 元素与其相关数据,可能会导致内存泄漏。这是因为普通的对象是强引用,当对象作为键时,即使元素被从 DOM 中移除,该对象仍然会保持对元素的引用,从而阻止垃圾回收器回收元素。
以下是一个示例,展示如果不使用 WeakMap 而是使用普通对象来存储关联的情况:
1 | const eventHandlers = {}; |
在这个示例中,当我们通过 removeClickHandler
移除事件处理程序时,对象 eventHandlers
中的引用仍然存在,即使数组为空,仍然会保留对空数组的引用,从而阻止垃圾回收器清理该对象。这可能会导致内存泄漏,因为无法回收与元素相关的数据
具体来说,eventHandlers
对象本身是一个普通的对象,它保持对所有关联事件处理程序数组的引用。如果不手动调用 delete eventHandlers[element]
,即使关联的数组为空,eventHandlers
对象仍然会保留对这些数组的引用。这就会阻止这些数组及其关联的元素被垃圾回收器清理。
换句话说,即使事件处理程序数组为空,只要 eventHandlers
对象本身还存在,与之关联的元素以及数组就不会被垃圾回收。这可能导致内存泄漏,因为即使不再需要这些数据,它们仍然会占用内存。
所以,在使用普通对象时,确保在不再需要关联数据时,通过 delete eventHandlers[element]
来手动清理对象中的关联数据,以防止内存泄漏。
总之,使用 WeakMap 来关联 DOM 元素与其相关数据可以有效避免内存泄漏,因为 WeakMap 的键是弱引用,当元素被移除时,相关数据会被自动清理。