ES6 Map和Set

ES6规范引入了新的数据类型Map和Set

Map 和 Objects 的区别

  • 一个 Object 的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值
  • Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
  • Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。
  • Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
  • Map 是 iterable 的,所以可以直接被迭代,而迭代一个Object需要以某种方式获取它的键然后才能迭代。
  • 在频繁增删键值对的场景下,Map表现更好。

参考文章

关于可迭代对象

  • 可参考可迭代对象 MDN,非常详细
  • Javascript中最常见的迭代器是ArrayString、Array、TypedArray、Map 和 Set 都是内置可迭代对象,因为它们的原型对象都拥有一个 Symbol.iterator 方法
  • Iterator 对象的**next()**:
    • 返回具有两个属性的对象value,这是序列中的 next 值;和 done ,如果已经迭代到序列中的最后一个值,则它为 true例子

Map 键值对二维数组

  • Map MDN很全面,可以直接看!
  • Map是一组键值对的结构,具有极快的查找速度。
    • 任何值(对象或者原始值) 都可以作为一个键或一个值
  • 使用场景:在频繁增删键值对的场景下,Map表现比普通obj更好
  • 【创建方法】用Map实现,只需要一个“名字”-“成绩”的对照表,可直接根据名字查找成绩:
    1
    2
    3
    4
    5
    let 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var myMap = new Map();
myMap.set(0, "zero");
myMap.set(1, "one");

// 将会显示两个 log。 一个是 "0 = zero" 另一个是 "1 = one"
for (var [key, value] of myMap) {
console.log(key + " = " + value);
}
for (var [key, value] of myMap.entries()) {
console.log(key + " = " + value);
}
/* 这个 entries 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组。 */

// 将会显示两个log。 一个是 "0" 另一个是 "1"
for (var key of myMap.keys()) {
console.log(key);
}
/* 这个 keys 方法返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键。 */

// 将会显示两个log。 一个是 "zero" 另一个是 "one"
for (var value of myMap.values()) {
console.log(value);
}
/* 这个 values 方法返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值。 */

forEach()

1
2
3
4
5
6
7
8
var myMap = new Map();
myMap.set(0, "zero");
myMap.set(1, "one");

// 将会显示两个 logs。 一个是 "0 = zero" 另一个是 "1 = one"
myMap.forEach(function(value, key) {
console.log(key + " = " + value);
}, myMap)

Map对象的操作

Map 与 Array的转换

1
2
3
4
5
6
7
8
9
10
let kvArray = [["key1", "value1"], ["key2", "value2"]];

// 使用常规的Map构造函数可以将一个二维键值对数组转换成一个Map对象
let myMap = new Map(kvArray);

// 使用Array.from函数可以将一个Map对象转换成一个二维键值对数组
console.log(Array.from(myMap)); // 输出和kvArray相同的数组

// 更简洁的方法来做如上同样的事情,使用展开运算符
console.log([...myMap]);

Map 的克隆

1
2
3
4
5
let original = new Map([
[1, 'one']
]);

let clone = new Map(original); // false. 浅比较 不为同一个对象的引用

Map 的合并

  • Map对象间可以进行合并,但是会保持键的唯一性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let 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
    17
    let 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去重
  • Set 的三个基本操作
    1
    2
    3
    4
    5
    6
    7
    // 【创建方法】直接创建一个空Set/提供一个Array作为输入
    let s = new Set(); // 创建方法1 空Set
    let s = new Set([1, 2, 3]); // 创建方法21, 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
    2
    let 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去重
  • 重复元素在Set中自动被过滤 (注意 数字3 和 字符串3 是不同的元素):
1
2
3
4
5
6
7
8
const arr = [1, 2, 3, 3, '3']
const s = new Set(arr);
s; // Set {1, 2, 3, "3"}

// 实际工作中还是需要转为数组而不是Set,所以可以如下操作:
console.log([...new Set(arr)]) // [1, 2, 3, '3']
//或
console.log(Array.from(new Set(arr))) // [1, 2, 3, '3']

数组相关的面试题

问题:给定一个有序数组,数组里有重复元素,要求原地操作数组,删除重复元素。
答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let arr = [1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 11, 22, 11, 22, 33];
function unique(arr) {
// 原地操作数组,删除重复元素
let len = 0; // len为不重复数的index,结果数组长度len+1
// 遍历原数组
for (let i = len; i < arr.length; i++) {
// 原数组中第一次找到的数组项放入数组前方,len长度+1
if (arr.indexOf(arr[i]) === i) {
arr[len] = arr[i];
len++;
}
// 相同的i+1继续查
}
// 去重后,删除数组后方多余数据项
arr.splice(len);
// 原地修改,不需要返回
}
// 针对 原地修改,不返回数组 的打印
unique(arr);
console.log(arr);// [1, 2, 3, 4, 5, 11, 22, 33]
  1. 设置len为不重复数组项的index(也就是将不重复数替换到 原数组[len] 位置上,这样就不用新开一个数组了)
  2. 原数组上循环i为循环的index
  3. 原数组中找arr[i]比对是否是该数组项的第一个位置,是则arr[i]赋给arr[len](也就是将不同的这个数放到原数组靠前的位置上),再len++(使得下一次替换的原数组index+1),不是就不管,i++进行下一次循环
  4. 全部去重后,使用splice(len)将len后面的多余数据删掉
    • 注意,最后一次符合if条件时len+1的位置还没用,所以删除时len最末尾也还属于 重复数组 ,一并删去

求并集,交集和差集

1
2
3
4
5
6
7
8
9
10
11
let a = new Set([1, 2, 3])
let b = new Set([2, 3, 4])

//并集
console.log(new Set([...a, ...b])) // Set(4) {1, 2, 3, 4}

//交集
console.log(new Set([...a].filter(v => b.has(v)))) // Set(2) {2, 3}

//差集
new Set([...a].filter(v => !b.has(v))) // Set(1) {1}

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:
    1. 弱引用键关联的数据: WeakMap允许您将弱引用的键与相应的值关联起来。这意味着,如果一个对象作为WeakMap的键,而这个对象没有其他强引用,垃圾回收器可以自由地回收这个对象,同时WeakMap中的对应键值对也会被移除
    2. 防止内存泄漏: 当您需要在不再需要时确保对象被垃圾回收,但又不想手动删除键值对时,WeakMap非常有用。这可以防止由于保留对不再需要的对象的引用而导致的内存泄漏问题。
    3. 隐私性和安全性: 如果您希望存储的数据不能被外部访问或篡改,WeakMap可以用于存储私有数据,因为WeakMap的键是弱引用,外部无法直接获取键来访问值。
  • WeakSet:
    1. 弱引用的值集合: WeakSet允许您存储一组值,这些值都是弱引用的。与WeakMap类似,如果一个对象作为WeakSet中的值,但没有其他强引用,那么这个对象可以被垃圾回收,同时WeakSet中的该值也会被移除。
    2. 不能避免重复值: 在一个 WeakSet 中可以多次存储同一个对象的弱引用。WeakSet 并不能自动去重
  • 使用场景:
    1. 缓存: WeakMap可以用于实现一种缓存机制,其中对象被用作键,而值是计算得到的结果。一旦这些对象(键)不再被其他部分强引用/强引用发生变化(例如将变量赋值为 null 或者离开了作用域,或者整个对象被替换=>引用地址发生变化),垃圾回收器可以自由地回收这些对象(键),缓存中的对应键值对也会被自动从WeakMap中移除,从而节省内存(例子见下)
      • 普通数组是强引用,可以用于缓存,但需要更多的手动管理,包括在不需要时手动从数组中移除缓存对象等
    2. DOM元素关联数据: 在处理DOM元素时,可以使用WeakMap来存储元素与相关数据之间的关联,当元素被从DOM中移除时,垃圾回收器会自动清理这些数据。
    3. 事件处理程序: 在为DOM元素添加事件处理程序时,可以使用WeakMap来存储事件处理函数,以避免内存泄漏。
    4. 防止内存泄漏: 当您需要在不再需要对象时确保对象被垃圾回收,而又不想手动清除相关数据时,WeakMap和WeakSet都是有用的选择。
  • 注意: 由于WeakMap和WeakSet的特性,它们并不适合所有场景。如果需要强引用并且确保对象不会被垃圾回收,那么应该使用Map和Set

缓存

当使用 WeakMap 来实现缓存机制时,通常是将某些昂贵的计算结果关联到特定的对象上,并且只要这些对象还有其他的强引用,那么计算结果就会被保留在缓存中。
其中对象被用作键,而值是计算得到的结果。
一旦这些对象(键)不再被其他部分强引用/强引用发生变化(例如将变量赋值为 null 或者离开了作用域,或者整个对象被替换=>引用地址发生变化),垃圾回收器可以自由地回收这些对象(键),从而自动清理缓存。

以下是一个使用 WeakMap 实现缓存机制的示例:

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
29
30
31
const cache = new WeakMap();

function calculateExpensiveResult(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}

// 模拟一个耗时的计算过程
const result = obj.value * 2;

cache.set(obj, result);
return result;
}

let keyObject = { value: 10 }; // 在全局作用域中定义对象

const result1 = calculateExpensiveResult(keyObject);
console.log(result1); // 输出: 20

// 修改了value,但对象的引用地址没有改变,所以在Map中算相同的对象作为键
// 这个计算结果会从缓存中获取,而不是重新计算
keyObject.value = 20
const result2 = calculateExpensiveResult(keyObject);
console.log(result2); // 输出: 20,读的缓存

// 改变对象引用地址,之前的 keyObject 失去引用
keyObject = { value: 30 };

// 垃圾回收器可以清理之前的 keyObject,cache 中的项也会被自动清理
const result3 = calculateExpensiveResult(keyObject);
console.log(result3); // 输出: 60,重新计算,因为 keyObject 已失去引用

补充了解:普通数组是强引用,可以用于缓存,但需要更多的手动管理,包括在不需要时手动从数组中移除对象等


DOM元素关联数据 防止内存泄漏

没有缓存的好理解啊,找了个例子,但没懂绑定handlers是为啥,以后有空回来再看看。

WeakMap 判断元素不再被引用是基于 JavaScript 垃圾回收机制中的弱引用概念。当一个对象不再有强引用指向它时,它会成为垃圾回收的候选对象,可以被垃圾回收器回收。

当使用 WeakMap 关联元素与数据时,在 WeakMap使用某个元素作为键,如果您不再有强引用指向这些元素(例如将变量赋值为 null 或者离开了作用域),那么这些元素将成为弱引用,WeakMap 将自动移除该键值对,从而触发与该元素关联的数据的清理,一旦垃圾回收器执行,与这些元素关联的数据也会被清理

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const eventHandlers = new WeakMap();

// 将给定的事件处理程序与特定的 DOM 元素关联起来,并确保事件处理程序能够在点击元素时被触发。同时,这个关联是通过 WeakMap 来建立的,因此在元素不再被引用时,事件处理程序数组会被自动清理,从而防止内存泄漏。
function addClickHandler(element, handler) {
// 检查 `eventHandlers` 中是否已经存在与给定元素关联的数组。如果不存在,就创建一个新的数组,并将元素作为键,数组作为值,使用 `eventHandlers.set(element, [])` 将它们关联起来。这是使用 WeakMap 来建立元素和事件处理程序数组的关联
if (!eventHandlers.has(element)) {
eventHandlers.set(element, []);
}

const handlers = eventHandlers.get(element); // 使用 `eventHandlers.get(element)` 来获取与元素关联的事件处理程序数组
handlers.push(handler); // 将新的事件处理程序 `handler` 添加到获取到的事件处理程序数组中

element.addEventListener('click', handler); // 使用 `element.addEventListener('click', handler)` 将事件处理程序绑定到 DOM 元素的 `click` 事件上,以便在点击元素时触发这个处理程序
}

function removeClickHandler(element, handler) {
// 检查是否存在与元素关联的事件处理程序数组
if (eventHandlers.has(element)) {
// 获取元素关联的事件处理程序数组
const handlers = eventHandlers.get(element);
// 在数组中查找要移除的事件处理程序
const index = handlers.indexOf(handler);
// 如果找到了要移除的事件处理程序
if (index !== -1) {
// 从数组中移除该事件处理程序
handlers.splice(index, 1);
// 从 DOM 元素的 'click' 事件中移除该处理程序
element.removeEventListener('click', handler);

/* 不需要手动delete,WeakMap 会在元素不再被引用时自动触发数据清理
if (handlers.length === 0) {
// 从 WeakMap 中删除与元素关联的数据。这样在元素不再被引用时,WeakMap 会自动将键值对从中移除,同时我们也手动保证了数据的清理
eventHandlers.delete(element);
} */
}
}
}

// getElementById 返回对拥有指定 ID 的第一个元素对象的引用
const button = document.getElementById('myButton');

function clickHandler() {
console.log('Button clicked');
}

addClickHandler(button, clickHandler);

// 在点击按钮后,会输出 'Button clicked'
button.click();

// 移除事件处理程序,此时关联的数据会被自动清理
removeClickHandler(button, clickHandler);

// 点击按钮后不再触发事件处理程序
button.click();

要注意的是,垃圾回收的执行时机由 JavaScript 引擎控制,不是立即发生的。这意味着在一个元素不再被引用之后,数据不会立即被清理。垃圾回收会在适当的时间点执行,清理不再有引用的对象和数据。


如果使用普通对象

如果不使用 WeakMap 而是使用普通的对象来关联 DOM 元素与其相关数据,可能会导致内存泄漏。这是因为普通的对象是强引用,当对象作为键时,即使元素被从 DOM 中移除,该对象仍然会保持对元素的引用,从而阻止垃圾回收器回收元素。

以下是一个示例,展示如果不使用 WeakMap 而是使用普通对象来存储关联的情况:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const eventHandlers = {};

function addClickHandler(element, handler) {
if (!eventHandlers[element]) {
eventHandlers[element] = [];
}

const handlers = eventHandlers[element];
handlers.push(handler);

element.addEventListener('click', handler);
}

function removeClickHandler(element, handler) {
if (eventHandlers[element]) {
const handlers = eventHandlers[element];
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
element.removeEventListener('click', handler);

/* 使用普通对象来手动关联数据,当不再需要这些数据时,可通过 delete 来手动清理关联的数据,以防止内存泄漏
// 检查是否还有剩余的事件处理程序在数组中
if (handlers.length === 0) {
delete eventHandlers[element]; // 手动删除关联的数据
}
*/
}
}
}

const button = document.getElementById('myButton');

function clickHandler() {
console.log('Button clicked');
}

addClickHandler(button, clickHandler);

// 在点击按钮后,会输出 'Button clicked'
button.click();

// 移除事件处理程序,但相关数据并没有被清理
removeClickHandler(button, clickHandler);

// 点击按钮后不再触发事件处理程序
button.click();

在这个示例中,当我们通过 removeClickHandler 移除事件处理程序时,对象 eventHandlers 中的引用仍然存在,即使数组为空,仍然会保留对空数组的引用,从而阻止垃圾回收器清理该对象。这可能会导致内存泄漏,因为无法回收与元素相关的数据

具体来说,eventHandlers 对象本身是一个普通的对象,它保持对所有关联事件处理程序数组的引用。如果不手动调用 delete eventHandlers[element],即使关联的数组为空,eventHandlers 对象仍然会保留对这些数组的引用。这就会阻止这些数组及其关联的元素被垃圾回收器清理。

换句话说,即使事件处理程序数组为空,只要 eventHandlers 对象本身还存在,与之关联的元素以及数组就不会被垃圾回收。这可能导致内存泄漏,因为即使不再需要这些数据,它们仍然会占用内存。

所以,在使用普通对象时,确保在不再需要关联数据时,通过 delete eventHandlers[element] 来手动清理对象中的关联数据,以防止内存泄漏。

总之,使用 WeakMap 来关联 DOM 元素与其相关数据可以有效避免内存泄漏,因为 WeakMap 的键是弱引用,当元素被移除时,相关数据会被自动清理。


,