# Set、WeakSet、Map 数据结构的应用及面试真题解析

TIP

本节内容我们开始学习 ES6 中的 Set、WeakSet、Map、WeakMap 数据结构、实例的方法和属性,构造函数的参数,在实际开发中的注意事项和应用。

Set

  • Set 的基本用法 ?
  • Set 成员的唯一性
  • Set 类型检测
  • Set 实例的方法和属性
  • Set 实例的遍历方法
  • Set 与解构赋值
  • Set 与扩展运算符
  • Set 在实际开发中的应用

WeakSet

  • WeakSet 的基本用法
  • WeakSet 成员特性
  • WeakSet 实例方法
  • WeakSet 成员持弱引用
  • Set 与 WeakSet 的区别,面试真题解析
  • WeakSet 在实际开发中的应用

Map

  • Map 是什么 ?
  • Map 实例的方法和属性
  • Map 构造函数的参数
  • Map 在实际开发中的注意事项
  • Map 在实际开发中的应用
  • Map 经典面试真题解析
  • Map 与 WeakMap 的区别 ?面试真题解析

# 一、Set 的核心基础

TIP

Set 是 ES6 中指供的一种新的数据结构,Set 对象允许你存储任何类型的唯一值

我们常把 Set 与数组来做对比,Set 和数组在存储数据时,最大的区别就在于 Set 的成员是唯一的,而数组是可以重复的。

Set 本身是一个构造函数,用来生成 Set 数据结构

# 1、Set 的基本用法

  • 创建 Set,并初始化成员

重点提示:

  • Set 函数可以接受一个数组(或者可迭代对象)作为参数,用来初始化成员
  • 常见的可迭代对象有:数组、arguments、NodeList、Map、HTMLCollection、String 类型
//  传入参数为数组,数组为可迭代对象
const s1 = new Set([1, 2, 3]);
console.log(s1); // Set(3) {1, 2, 3}

// 传入参数为NodeList,NodeList为可迭代对象
const list = document.querySelectorAll("ul li");
const s2 = new Set(list);
console.log(s2); // Set(4) {li, li, li, li}

// 传入参数为arguments,arguments为可迭代对象
function fn() {
  const s = new Set(arguments);
  console.log(s); // Set(3) {1, 2, 'ab'}
}
fn(1, 2, "ab");

// 对象为非迭代对象
const obj = {
  a: 1,
  b: 2,
  arr: [3, 4, 5],
};
// 但为对象添加迭代器,那对象也可以作为Set的参数
obj[Symbol.iterator] = function* () {
  for (let key in this) {
    yield this[key];
  }
};
const s3 = new Set(obj);
console.log(s3); // Set(3) {1, 2, Array(3)}
  • 创建 Set,利用 add 方法添加成员
const s = new Set();
// add方法向Set中添加成员
s.add(1);
s.add(2);
s.add(3);
console.log(s);

# 2、Set 成员的唯一性

TIP

Set 中的成员必需是唯一的,其内部判断两个值是否相同类传于精确相等运算符 === 主要的区别 Set 中认为 NaN 和 NaN 是相等的,同时 +00-0 也是相等的。

const s = new Set([1, 2, 3, 3, NaN, 4, NaN]);
// 因为Set成员必需是唯一的,所以3和NaN只保留了一个
console.log(s); // Set(5) {1, 2, 3, NaN, 4}
const s = new Set([{ a: 1 }, 1, { a: 1 }, ["A"], +0, 0, -0]);
// 对象保存的是堆内存中的地址,所以长的一样地址不一要,也是不一样的,在同值相等和精确相等中算法中,+0,0,-0 认为是同一个值
console.log(s);

image-20230114161453332

# 3、Set 类型检测

  • 用 typeof 检测返回 object
const s = new Set([1, 2, 3]);
console.log(typeof s); // object
  • 利用 Object 原型上的 toString 方法来检测
const s = new Set([1, 2, 3]);
// Object原型上的toString方法来检测
let type = Object.prototype.toString.call(s);
console.log(type); // [object Set]
// 获取数据的类型
type = type.split(" ")[1].slice(0, -1).toLowerCase();
console.log(type); // set
  • 利用 Object 原型上的 toString 方法来判断不同的数据类型
function getType(x) {
  let originType = Object.prototype.toString.call(x); // '[object type]'
  let type = originType.split(" ")[1].slice(0, -1); //用空格来分隔字符串
  return type.toLowerCase(); // 将分格出来的类型,统一转成小写字
}
console.log(getType(new Set([1])));
console.log(getType([]));
console.log(getType({}));
console.log(getType(function () {}));
console.log(getType("ss"));

# 4、Set 实例的属性

属性 说明
size 返回 Set 实例的成员总数
  • size 返回 Set 实例的成员总数
const s1 = new Set([1, 2, 3]);
console.log(s1.size); // 3
const s2 = new Set(["a", "b", "c", "d", "e"]);
console.log(s2.size); // 5

# 5、Set 实例的方法

方法名 说明
add(value) 向 Set 实例尾部添加一个成员(值),返回该 Set 实例本身(即支持链式调用)
delete(value) 删除 Set 实例的某个成员(值),删除成功返回 true,失败返回 false
has(value) 判断某个值是否为 Set 实例的成员,如果是返回 true,否则返回 false
clear() 删除 Set 实例的所有成员,没有返回值
  • add(value) 添加成员,返回值为该实例对象本身,即 add 方法支持链式调用
const s = new Set([1, 2]);
s.add("a");
s.add(1); // 1成员本身有,所以添加失败
// add方法,支持链式调用
s.add("c").add("d");
console.log(s); // Set(5) {1, 2, 'a', 'c', 'd'}

// add 的返回值实例对象本身
const s = new Set(["a", "b"]);
console.log(s.add(4)); // Set(3) {'a', 'b', 4}
  • delete(value) 删除成员, 删除成功返回 true,失败返回 false
const s = new Set([1, 2, 3, 4]);
s.delete(1); // 删除成员1
s.delete(1); // 删除不存在的元素,不会报错
s.delete(3); // 删除成员3
console.log(s); // Set(2) {2, 4}

const s2 = new Set(["a", "b", "c"]);
// delete()方法的返回值,删除成功返回true,失败返回false
console.log(s2.delete("a")); // true
console.log(s2.delete("d")); // false
  • has(value) 判断是否有某个成员,如果有,返回 true,没有返回 false
const s = new Set(["a", "b", "c", 1]);
console.log(s.has("a")); // true
console.log(s.has(2)); // false
  • clear 清除所有成员,没有返回值
const s = new Set(["a", "b", "c", 1]);
s.clear();
console.log(s); // Set(0) {size: 0}

温馨提示:

Set 实例对象没有办法像数组一样,通过下标来访问和设置成员。只能通过以上方式添加、删除成员。

第次添加只能添加到最后一个成员,删除可以删除任意成员。

# 6、Set 实例的遍历方法

遍历方法 说明
keys() 返回值与 values() 方法的返回值一样,因为 Set 对象没有键名,所以返回成员值
values() 返回一个新的遍历器(迭代器)对象,该对象包含 Set 对象中的按插入顺序排列的所有元素的值。
entries() 返回一个新的遍历器(迭代器)对象,该对象包含 Set 键值对集合的,其顺序与 Set 成员插入顺序一致
forEach(callBack[,thisArg]) 按照 Set 成员的插入顺序,为 Set 对象中的每一个成员调用一次 callBack 回调函数,如果传入了 thisArg 参数,回调函数内的 this 指向 thisArg

# 7、for ... of 循环

TIP

在学习 Set 实例的遍历方法前,我们简单了解 for...of 循环,它是 ES6 新增的用来遍历可迭代对象的循环语句。

只要数据是可迭代对象,就可以利用 for...of 循环来遍历它的成员。

常见的可迭代对象有:数组、arguments、NodeList、HTMLCollection、Set、Map、String 类型

HTMLCollection、String  // 数组是可迭代对象
const arr = [1, 2, 3, 4];
for (let item of arr) {
    console.log(item); // 1 2 3 4
}

// Set是可迭代对象
const set = new Set([1, 22, 33, 4]);
for (let item of set) {
    console.log(item); // 1 22 33 4
}

// 关于arguments 、NodeList 、Map、HTMLCollection、String  大家自行测试

# 7.1、keys 和 values 方法

TIP

keys 和 values 方法返回的都是迭代对象,由于 Set 结构没有键名,只有键值(或者说键名与键值是同一个值)所以 keys 方法和 values 方法的返回值完全一致。

const s = new Set(["a", "b", "c"]);
const keys = s.keys();
console.log(keys); // keys为迭代器对象
for (let key of keys) {
  console.log(key); // a b c
}

const values = s.values(); // values为迭代器对象
console.log(values);
for (let key of keys) {
  console.log(key); // a b c
}

迭代器对象,再次遍历是无效的

const s = new Set(["a", "b", "c"]);
const keys = s.keys();
console.log(keys); // keys为迭代器对象
for (let key of keys) {
  console.log(key); // a b c
}

// 迭代器对象,再次遍历是无效的,以下代码,啥也不会执行
for (let key of keys) {
  console.log(key);
}

// 所以每次迭代都会创建一个新的迭代器对象
for (let key of s.keys()) {
  console.log(key); // a b c
}
for (let key of s.keys()) {
  console.log(key); // a b c
}

Set 的实例默认可遍历,本质是因为遍历器生成函数是它的 values 方法

Set.prototype[Symbol.iterator] === Set.prototype.values; // true
// 当利用 for...of 来遍历set实例时,其内部会调用values方法,生成一个迭代器对象,来供 for...of 使用,所以我们遍历Set对象,可以直接遍历,不用调用他的values方法

# 7.2、entries()

TIP

返回一个新的遍历器(迭代器)对象,该对象包含 Set 键值对集合的,其顺序与 Set 成员插入顺序一致

const s = new Set([1, 2, 3, 4]);
for (let item of s.entries()) {
  console.log(item);
}

image-20230114181805434

# 7.3、 forEach( )方法

TIP

按照 Set 成员的插入顺序,为 Set 对象中的每一个成员调用一次 callBack 回调函数。

如果传入了 thisArg 参数,回调函数内的 this 指向 thisArg

forEach(callBack[,thisArg])
forEach(function(value,key,s){}[,thisArg])
// 回调函数中的三个参,value键值,key键名,s为实例对象本身
const s = new Set(["a", "b", "c"]);
s.forEach(function (value, key, s) {
  console.log(value);
  console.log(key);
  console.log(s);
});

image-20230114182415236

  • 如果传入了第二个参数,则回调函数内部的 this 指向第二个参数,前提是回调函数只能是普通函数。如果回调函数为箭头函数,则 this 更改会失败
const s = new Set(["a"]);
const obj = { a: 1 };
s.forEach(function (value) {
  console.log(this);
}, obj);

// 回调函数为箭头函数,并不会更改this指向
s.forEach((value) => {
  console.log(this);
}, obj);

image-20230114182730829

# 8、Set 与解构赋值

TIP

我们之前讲数组的解构赋值时提到,=等号的右边只要是可迭代对象都可以利用数组模式来解构赋值,那 Set 也可以。

const [x, y, z] = new Set(["a", "b", "c"]);
console.log(x, y, z); // a b c

# 9、Set 与扩展运算符

TIP

可以利用...扩展运算符将 Set 实例对象在数组中展开

const arr = [...new Set([1, 2, 3])];
console.log(arr);

# 10、Set 实例如何应用数组的方法操作成员

TIP

如果想让 Set 实例应用数组相关的方法,按以下三步来操作

  • 第一步:把 Set 实例在一个空数组中展开
  • 第一步:让数组调用相关方法操作其成员
  • 第三步:操作后再将数组作为 Set 构造函数的参数,创建一个新的 Set 实例
// 将Set中所有大于5的元素留下,其它的都删除
let s = new Set([1, 5, 2, 9, 10]);
s = new Set([...s].filter((value) => value > 5));
/*  相当于
	第一步:[...s]
	第二步: [1,5,2,9,10].filter(...)
   	第三步:  new Set([9,10])
*/
console.log(s); // Set(2) {9, 10}

# 二、Set 在实际开发中的应用

TIP

以下情况,我们可以考虑使用 Set

  • 数组或字符串去重时
  • 不需要通过下标访问,只需要遍历时
  • 为了使用 Set 提供的方法和属性时(add、delete、clear、has、forEach、size 等)

# 1、数组去重

let arr1 = [1, 2, 3, 3, 4, 5, 3, 2];
arr1 = [...new Set(arr1)];
console.log(arr1); // [1, 2, 3, 4, 5]

# 2、求并集

TIP

并集:将两个数组合并,然后去除重复项

const arr1 = [2, 3, 5, 6, 5, 7, 8];
const arr2 = [1, 3, 5, 7, 7, 9, 10, 12];
// 求并集  合并arr1与arr2,同时去重
const union = new Set([...arr1, ...arr2]);
console.log(union);

# 3、求交集

TIP

交集:求两个数组都有的项,然后再去除重复项

image-20230114202919790

const arr1 = [2, 3, 5, 6, 5, 7, 8];
const arr2 = [1, 3, 5, 7, 7, 9, 10, 12];
// 找出arr1在arr2中有的项
let result = arr1.filter((item) => {
  return new Set(arr2).has(item);
});
// 去除重复项
result = new Set(result);
console.log(result); // Set(3) {3, 5, 7}
// 简写形式
let result = new Set(
  arr1.filter((item) => {
    return new Set(arr2).has(item);
  })
);

# 4、求差集

TIP

求差集:求 arr1 在 arr2 中没有的项,然后再去重

image-20230114202956503

const arr1 = [2, 3, 5, 6, 5, 7, 8];
const arr2 = [1, 3, 5, 7, 7, 9, 10, 12];
// 找出arr1在arr2中有的项
let result = new Set(
  arr1.filter((item) => {
    return !new Set(arr2).has(item);
  })
);
console.log(result); // Set(3) {3, 5, 7}

# 三、WeakSet 数据结构

TIP

WeakSet 结构与 Set 类似,也是不重复值的集合,但是,他与 Set 存在以下两个方面的区别

  • WeakSet 的成员只能是对象,而不能像 Set那样,可以是任何类型的任意值
  • WeakSet 持弱引用:集合中对象的引用为弱引用。如果没有其它的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。

接下来我们从最基本的 WeakSet 创建开始,然后再一步步深入了解它 !

# 1、WeakSet 的基本用法

TIP

WeakSet 是一个构造函数,可以通过 new WeakSet() 方式创建 WeakSet 数据结构

  • 创建同时,初始化成员,其构造函数的参数必须是一个数组或可迭代对象
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const arr = [1, 2];
const ws = new WeakSet([obj1, obj2, arr]);
console.log(ws);

image-20230114210750356

温馨提示

  • WeakSet 成员的顺序是随机的,并不是按添加时的顺序排列显示的,这个和 Set 是有区别的。
  • Set 是按添加的顺序来显示的。但这个特性对 WeakSet 没有任意影响,因为他的成员不可遍历
  • 创建后,调用 add 方法添加成员
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const arr = [1, 2];
const ws = new WeakSet();
ws.add(obj1); // 添加成员
ws.add(obj2);
ws.add(arr);
console.log(ws);

image-20230114210752206

# 2、WeakSet 成员特性

TIP

  • WeakSet 成员只能是唯一的,同时只能是对象,不能是其它类型
  • 所以在 new WeakSet() 的同时,初始化成员,其数组(可迭代对象)成员只能是对象类型。
const obj1 = { a: 1 };
const obj2 = obj1;
const obj3 = { b: 2 };
const obj4 = { b: 2 };
// obj1和obj2 是同一个对象,所以只能添加1个,obj3和obj4只是长的一样,本质是两个不同对象
const ws = new WeakSet([obj1, obj2, obj3, obj4]);
console.log(ws);

image-20230114211158482

let a = 1;
const ws = new WeakSet();
ws.add(a); // 报错

# 3、WeakSet 实例方法

方法 说明
add(value) 向 WeakSet 中添加成员,返回当前对象,即支持链式调用
delete(value) 删除 WeakSet 中指定的成员,删除成功返回
has(value) 判断 WeakSet 中是否有对应的成员,有返回 true,没有返回 false
  • add(value) 向 WeakSet 中添加成员,返回当前对象,即支持链式调用
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const ws = new WeakSet();
ws.add(obj1).add(obj2);
console.log(ws);

image-20230114221648477

  • delete(value) 删除 WeakSet 中指定的成员,删除成功返回 true,失败返回 false
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const ws = new WeakSet([obj1, obj2]);
ws.delete(obj1);
console.log(ws);

image-20230114221618407

  • has 判断 WeakSet 中是否有对应的成员,有返回 true,没有返回 false
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const ws = new WeakSet([obj1, obj2]);
console.log(ws.has(obj1)); // true
console.log(ws.has({})); // false

# 4、WeakSet 成员持弱引用

TIP

WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。

也就是说,如果该对象没有被其它对象引用,那么垃级回收机制就会自动回收该对象所占用的内存,不考虑该对象是否还在 WeakSet 中。

# 4.1、垃级回收

TIP

官方文档垃圾回收参考 (opens new window)

我们说垃圾回收机制主要有两种:引用计数和标记清除。目前浏览器主要采用的是标记清除这种算法。但垃圾回收本质还是要依赖于引用的概念,在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。

如果一个值的引用次数不为 0,垃圾回收机制就不会释放这块内存。只有当这个值的引用次数为 0 时,垃圾回收机制才会释放这块内存。

let obj = { a: 1 }; // obj对对象{a:1} 引用一次,计数+1
let obj2 = obj; // obj2对对象{a:1} 引用一次,计数+1,引用次共2次
obj = null; // 解除obj对{a:1}对象的引用,计数-1,引用次数共1次
obj2 = null; // 解除obj对{a:1}对象的引用,计数-1,引用次数共0次
// 当引用次数为0时,过段时间垃圾回收机制回收垃圾时,就会把 {a:1} 所占用的内存回收

# 4.2、WeakSet 中的对象都是弱引用

TIP

WeakSet 中对象都是弱引用,所以在引用计数时,引用次数并不会 +1

let obj = { a: 1 }; // obj对对象{a:1} 引用一次,计数+1
let ws = new WeakSet([obj]); // 这里对{a:1}的引用并不会被计入,计数+0
let obj2 = obj; // obj对对象{a:1} 引用一次,计数+1  共引用2次
obj2 = null; // 解除obj对{a:1}对象的引用,计数-1,引用次数共1次
obj = null; // 解除obj对{a:1}对象的引用,计数-1,引用次数共0次
// 当引用次数为0时,过段时间垃圾回收机制回收垃圾时,就会把{a:1}所占用的内存回收
console.log(ws); // 在制新时,有时ws中有内容,有时没有,是因为垃圾回收器回收垃圾的时间是不确定的

以下情况,WeakSet 中根本没有内容,因为 WeakSet 中的对象创建后就没有被引用过一次,所以引用计数一直是 0

const ws = new WeakSet([{ a: 1 }, { b: 2 }, [1, 3]]);
console.log(ws);

image-20230114220308185

我们在频繁刷新浏览器时,有时能在控制台看到对应内容,是因为对象创建好后,垃圾回收器还没来的急回收。

// 对比Set,其成员一直都在
const s = new Set([{ a: 1 }, { b: 2 }, [1, 3]]);
console.log(s);

image-20230114220234357

# 4.3、WeakSet 注意事项

TIP

  • 因为 WeakSet 中的对象都是弱引用,只要外部对 WeakSet 中的对象没有引用,这个对象就会被垃圾回收掉。
  • 所以 WeakSet 实例对象没有 size 属性和 forEach 方法,不允许遍历,因为无法保存成员的存在。
const obj1 = { a: 1 };
const ws = new WeakSet([obj1]);
console.log(ws.size); // undefined
ws.forEach((element) => {}); // 报错

# 5、总结 Set 与 WeakSet 的区别(面试题)

相同点 说明
成员唯一性 Set 和 WeakSet 中的成员都是唯一的,不能重复
实例方法 Set 和 WeakSet 都有 add、deleted、has 方法
不同点 说明
成员类型 Set 中的成员可以是任意类型,而 WeakSet 中的成员只能是对象
成员的引用 Set 中成员的引用是强引用,而 WeakSet 中成员的引用是弱引用
成员的遍历 Set 中的成员可 forEach 或 for ... of 可遍历的,而 WeakSet 中的成员是不可遍历,也不可枚举的
实例方法 Set 成员有 keys、values、entries 方法
解构赋值 Set 实例对象为可迭代对象,可以用数组模式解构赋值,而 WeakSet 不是可迭代对象,所以不能解构赋值
扩展运算符 Set 实例对象为可迭代对象,可以用...扩展运算符在数组中展开,而 WeakSet 不是可迭代对象,不能用...扩展运算符在数组中展开。

# 四、WeakSet 实际应用

TIP

如果我们希望对象身上的方法在被调用时,其内部的 this 永远指向该对象,而不能是其它对象。也就是bind、call、apply都没有办法更改其内部的 this 指向。

那我们就需要在用构造函数创建该对象时,把创建出来的实例添加到 WeakSet 中,然后在对象的方法中验证其 this 是否在 WeakSet 中,如果不在,则表明 this 指向有问题,抛出错误,如果存在,代码正常执行

const ws = new WeakSet();
function Person(name, age) {
  this.name = name;
  this.age = age;
  ws.add(this);
}
Person.prototype.sayHello = function () {
  if (!ws.has(this))
    throw new TypeError("sayHello方法只能在Person的实例上调用");
  console.log(`大家好,我是${this.name},今年${this.age}岁了`);
};

p = new Person("清心", 23);
p.sayHello();
// p.sayHello.call({ name: "icoding", age: 33 }); // 报错
p = null; // 将p对象销毁
console.log(ws); // 其内部的引用也消失,垃圾回收将对象回收掉了

上面将实例对象保存在 WeakSet 中,WeakSet 中保持对 p 的弱引用,所以当p = null时,对象没有其它相关的引用,引用次数为 0,所以垃圾回收就会将其从内存中回收掉。

  • 如果不用 WeakSet 而改用 Set 来存储,则有可能会引发内存泄露,因为 Set 中还保持着对对象的强引用,垃圾回收并不会将对象回收掉,需要我们手动将对象从 Set 中删除才可以。
const ws = new Set(); // Set中保持的是强引用
function Person(name, age) {
  this.name = name;
  this.age = age;
  ws.add(this);
}
Person.prototype.sayHello = function () {
  if (!ws.has(this))
    throw new TypeError("sayHello方法只能在Person的实例上调用");
  console.log(`大家好,我是${this.name},今年${this.age}岁了`);
};

p = new Person("清心", 23);
p.sayHello();
// p.sayHello.call({ name: "icoding", age: 33 }); // 报错
p = null; // 将p对象销毁
console.log(ws); // 但因为ws中保存着对对象p的引用,所以垃圾回收并没有回收p,一直在内存中存着

image-20230116181814414

  • 所以需添加 destory 方法,手动将对象从 ws 中的删除,这样当p = null时,p 的引用次数为 0,垃圾回收才会将对象回收掉
const ws = new Set();
function Person(name, age) {
  this.name = name;
  this.age = age;
  ws.add(this);
}
Person.prototype.sayHello = function () {
  // 判断this是否为Person构造出来的实例
  if (!ws.has(this))
    throw new TypeError("sayHello方法只能在Person的实例上调用");
  console.log(`大家好,我是${this.name},今年${this.age}岁了`);
};

// 添加方法,手动将对象从ws中移出
Person.prototype.destory = function () {
  ws.delete(this);
};

p = new Person("清心", 23);
p.sayHello(); // 正常输出结果
p.destory(); // 在对对象p销毁前,先要将Set中对他的引用切换,即在Set中将其删除
p = null; // 将p对象销毁
console.log(ws); // 但因为ws中保存着对对象p的引用,所以垃圾回收并没有回收p,一直在内存中存着

image-20230116175732578

# 五、Map 的核心基础

TIP

深入浅出 Map 是什么,Map 实例的属性和方法,Map 构造函数的参数,Map 在实际开发中的注意事项和应用。

# 1、为什么引入 Map

对象本身的局限性

JavaScript 的对象(Object)本质上是键值对的集合,但是其键名只能是字符串Symbol类型,不能是其它数据类型,这给它的使用带来了很大的限制。

如果我想将一个 DOM 节点和节点对应的样式,以键值对的形式存到对象中,是没办法存的,因为 DOM 节点会被转换为字符串类型,转换为[object HTMLDivElement]

const box = document.querySelector(".box");
const obj = {
  [box]: "width:100px;height:100px;background:red",
};
for (let key in obj) {
  console.log(key); // [object HTMLDivElement]
}

Map 的强大点

为了解决这个问题,ES6 提供了 Map 数据结构,它类似于对象,也是键值对集合,但是他的“键”可以是 任意的数据类型

如果说 Object 结构提供的是 “字符串 --> 值” 的映身,那 Map 结构提供的是 “值 ---> 值" 的映射

<div class="box"></div>
<script>
  const box = document.querySelector(".box");
  const map = new Map();
  map.set(box, "width:100px;height:100px;background:red");

  for (let [el, css] of map) {
    console.log(el);
    console.log(css);
    el.style.cssText = css;
  }
</script>

image-20230114232155985

image-20230114231912538

# 2、Map 的基本用法

TIP

Map 是一个构造函数,可以用他来创建 Map 数据结构。

# 3、new Map() 方式创建,随后用 set 方法添加成员

const map = new Map();
map.set({ age: 1 }, "清心");
map.set({ age: 33 }, "icoding");
console.log(map);

image-20230114233348400

# 3.1、new Map() 方式创建,并初始化成员

TIP

Map 构造函数接受二维数组作为参数,二维数组的每一项是一个双元素数组

// 以下代码创建map实例时就指定了两个键,分别是{ age: 1 } 和{ age: 33 },其对应值是"清心"和"icoding"
const map = new Map([
  [{ age: 1 }, "清心"],
  [{ age: 33 }, "icoding"],
]);
console.log(map);

上面代码内部实际执行过程如下

const arrs = [
  ["username", "清心"],
  [{ age: 33 }, "icoding"],
];

const map = new Map();
// 遍历数组中的每个成员,利用数组的解构赋值把成员取出来,添加到map中
for (let [key, value] of arrs) {
  map.set(key, value);
}
console.log(map);

重点

其实任何可迭代对象,只要可迭代对象返回的每个成员都是一个类似双元素数组的数据结构,都可以作为 Map 构造函数的参数。

// 用Set来作为Map构造函数的参数
const s = new Set([
  ["foo", () => "foo"],
  ["bar", () => "bar"],
]);
const m = new Map(s); // Map(2) {'foo' => ƒ, 'bar' => ƒ}

// 可以用Map来作为Map构造函数的参数
const map = new Map([
  ["a", 1],
  ["b", 2],
]);
const m = new Map(map);
console.log(m); // Map(2) {'a' => 1, 'b' => 2}

为对象添加iterator接口,把对象变成一个可迭代对象,同时返回值为 一个双元素数组。这样所有对象都可以做为Map()构造函数的参数

Object.prototype[Symbol.iterator] = function* () {
  for (let key in this) {
    yield [key, this[key]];
  }
};

const obj = {
  a: 11,
  b: [1, 2, 3],
};

const m = new Map(obj);
console.log(m); // Map(2) {'a' => 11, 'b' => [1, 2, 3]}

# 4、Map 中键的唯一性

TIP

在 Map 中,键名是唯一的。键的比较是基于零值相等算法 ,认为 NaN 和 NaN 是相等的,同时 0-0+0 也是相等的,其它判断和===严格相等一样

JavaScript 中的相等性判断,点击查看官方文档 (opens new window)

const map = new Map();
map.set(NaN, 1);
map.set(NaN, 2);
map.set(0, "a");
map.set(-0, "b");
map.set(+0, "c");
map.set({ a: 1 }, 1);
map.set({ a: 1 }, 2);
console.log(map); // Map(4) {NaN => 2, 0 => 'c', {…} => 1, {…} => 2}

# 5、Map 键的顺序

TIP

Map 中的键是有序的。其顺序为插入时的顺序。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。

我们知道,对象的键是无序的,很多时候我们想以插入的顺序来访问对应的键时,对象就没法实现。

const obj = {
  a: 1,
  b: 2,
  ["0"]: "a",
  ["1"]: "b",
};
console.log(obj);

const m = new Map([
  ["a", 1],
  ["b", 2],
  ["0", "a"],
  ["1", "b"],
]);
console.log(m);

image-20230115003450864

# 6、Map 类型的检测

const m = new Map();
console.log(typeof m); // object

let type = Object.prototype.toString.call(m);
console.log(type); // [object Map]

# 7、Map 的实例属性

属性 说明
size 返回 Map 结构的成员总数
const m = new Map([
  ["a", 1],
  ["b", 2],
  ["0", "a"],
  ["1", "b"],
]);
console.log(m.size); // 4

# 8、Map 的实例方法

方法 说明
set(key,value) 设置Map对应的键值,并返回当前Map对象,所以set方法支持链式调用
get(key) 返回指定键key对应的值,若不存在,则返回undefined
has(key) 判断Map中是否存在指定的键,有返回true,没有返回false
delete(key) 根据键名,删除Map中指定的键值对。删除成功返回true,否则返回false
clear() 移除 Map 对象中所有的键值对。

# 9、set(key,value)

TIP

设置Map对应的键值,并返回当前Map对象,所以set方法支持链式调用

const map = new Map();
map.set("{a:1}", "obj");
map.set("name", "清心");
console.log(map); // Map(2) {'{a:1}' => 'obj', 'name' => '清心'}

向 map 中插入相同的键时,其后插入的会覆盖先插入的

const map = new Map();
map.set("{a:1}", "obj");
map.set("name", "清心");
map.set("name", "icoding"); // 相同键,后面的会覆盖前面的
console.log(map); // Map(2) {'{a:1}' => 'obj', 'name' => 'icoding'}

# 9.1、get(key)

TIP

返回指定键 key 对应的值,若不存在,则返回undefined

const key1 = "{a:1}";
const key2 = "name";
const map = new Map([
  [key1, "a"],
  [key2, "清心"],
]);
const value = map.get(key1);
console.log(value); // a

# 9.2、has(key)

TIP

判断Map中是否存在指定的键,有返回true,没有返回false

const key1 = "{a:1}";
const key2 = "name";
const map = new Map([
  [key1, "a"],
  [key2, "清心"],
]);
console.log(map.has(key1)); //true

# 9.3、delete(key)

TIP

根据键名,删除Map中指定的键值对。删除成功返回true,否则返回false

const key1 = "{a:1}";
const key2 = "name";
const map = new Map([
  [key1, "a"],
  [key2, "清心"],
]);
map.delete(key1); //true
console.log(map); // Map(1) {'name' => '清心'}

# 9.4、clear()

TIP

移除 Map 对象中所有的键值对

const key1 = "{a:1}";
const key2 = "name";
const map = new Map([
  [key1, "a"],
  [key2, "清心"],
]);
console.log(map.clear()); // undefined
console.log(map); // Map(0) {size: 0}

# 10、Map 的遍历方法

方法 说明
keys() 返回一个新的迭代对象,其中包含 Map 对象中所有的键,其顺序为 Map 对象插入成员时的顺序排列
values() 返回一个新的迭代对象,其中包含 Map 对象中所有的值,其顺序为 Map 对象插入成员时的顺序排列
entries() 返回一个新的迭代对象,其为一个包含 Map 对象中所有键值对的 [key, value] 数组,其顺序为 Map 对象插入成员时的顺序排列
forEach() 以插入的顺序对 Map 对象中存在的键值对分别调用一次 callbackFn。如果给定了 thisArg 参数,这个参数将会是回调函数中 this 的值。

重点强调

Map 中的内容是有顺序的,其遍历出来的元素顺序与插入时的顺序是一致的

# 10.1、keys() 方法

TIP

返回一个新的迭代对象,其中包含 Map 对象中所有的键,其顺序为Map对象插入成员时的顺序排列

const map = new Map([
  ["name", "清心"],
  ["age", 33],
  [0, null],
  [null, "清空"],
]);
// map.keys()  返回一个新的迭代对象,可以用for...of来遍历
for (let key of map.keys()) {
  console.log(key);
}

image-20230119140407168

# 10.2、values() 方法

TIP

返回一个新的迭代对象,其中包含 Map 对象中所有的值,其顺序为Map对象插入成员时的顺序排列

const map = new Map([
  ["name", "清心"],
  ["age", 33],
  [0, null],
  [null, "清空"],
]);
// map.values()  返回一个新的迭代对象,可以用for...of来遍历
for (let value of map.values()) {
  console.log(value);
}

image-20230119140233516

# 10.3、entries() 方法

TIP

返回一个新的迭代对象,其为一个包含 Map 对象中所有键值对的 [key, value] 数组,其顺序为Map对象插入成员时的顺序排列

const map = new Map([
  ["name", "清心"],
  ["age", 33],
  [null, "清空"],
]);
// map.keys()  返回一个新的迭代对象,可以用for...of来遍历
for (let key of map.entries()) {
  console.log(key);
}

image-20230116190625593

利用数组的解构赋值,解构出对应的 key 和 value

const map = new Map([
  ["name", "清心"],
  ["age", 33],
  [null, "清空"],
]);
// [key,value]用来解构每次遍历返回的数组
for (let [key, value] of map.entries()) {
  console.log(key, value);
}

image-20230116190757330

重点提示

Map 结构的默认遍历接口(Symbol.iterator属性)就是entries方法

Map.prototype[Symbol.iterator] === Map.prototype.entries;

# 10.4、forEach() 方法

TIP

以插入的顺序对 Map 对象中存在的键值对分别调用一次 callbackFn。如果给定了 thisArg 参数,这个参数将会是回调函数中 this 的值。

forEach(function (value, key, map) {}); // value 键值  key 键名   map 当前遍历的map对象
forEach(function (value, key, map) {}, thisArg); // thisArg 更改回调函数中的this指向
const map = new Map([
  ["name", "清心"],
  ["age", 33],
  [null, "清空"],
]);
map.forEach(function (value, key, map) {
  console.log(value, key, map);
});

image-20230116191324417

重点提示:

要更改 forEach(callBack,thisArg) 中 callBack 回调函数中的 this 指向 thisArg,回调函数不能是箭头函数,只能是普通函数

const map = new Map([["name", "清心"]]);
const obj = { a: 1 };
// 普通函数中,this指向 obj
map.forEach(function () {
  console.log(this); // obj
}, obj);

// 箭头函数中,this没有被改变,指向window
map.forEach(() => {
  console.log(this); // window
}, obj);

# 11、Map 与解构赋值

TIP

Map 为可迭代对象,所以 Map 可以按数组的解构赋值模式来解构

const map = new Map([
  ["a", 1],
  ["b", 2],
]);

const [x, y] = map;
console.log(x); //  ['a', 1]
console.log(y); //  ['b', 2]

const [[a, b], [c, d]] = map;
console.log(a, b); // a 1
console.log(c, d); // b 2

# 12、Map 与扩展运算符

  • 利用扩展运算符可以将 Map 结构转换成对应的二维数组
const map = new Map([
  ["a", 1],
  ["b", 2],
]);

const arr = [...map];
console.log(arr); // [['a', 1],['b', 2]]
const [arr1, arr2] = [...map];
console.log(arr1); // ['a', 1]
console.log(arr2); // ['b', 2]
  • Map 对象与 Map 或数组合并时,如果有重复的键值,则后面的会覆盖前面的。
const map1 = new Map([
  ["a", 1],
  ["b", 2],
]);
const map2 = new Map([
  ["a", "{a:1}"],
  ["c", 3],
  ["d", "d"],
]);

const arr = ["c", null];
const map3 = new Map([...map1, ...map2, arr]);
console.log(map3); // Map(4) {'a' => '{a:1}', 'b' => 2, 'c' => null, 'd' => 'd'}

温馨提示:

[...map]可以将 map 转换为数组 new Map(二维数组)可以将二维数组转换为Map

# 13、Map 使用数组的所有方法

TIP

Map 本身没有太多的方法用来操作成员,不过我们可以利用扩展运算符将 Map 在数组中展开,然后利用数组的方法来操作其成员,操作完成后再将数组作为 Map 构造函数的参数。

利用数组的 filter 方法来过滤 Map 中所有价格大于 10 元的菜

let arrs = [
  ["白菜", 2.0],
  ["萝卜", 3.4],
  ["西蓝花", 5.8],
  ["茄子", 7.8],
];
let map = new Map(arrs);
// 1、先将map转数组
//   arrs = [...map];
// 2、利用数组的方法
//   arrs = arrs.filter(([key, value]) => value > 5);
// 3、将数组作为map构造函数的参数
//   map = new Map(arrs);
//   console.log(map);

// 以下是简写形式
map = new Map([...map].filter(([key, value]) => value > 5));
console.log(map);

# 六、Map 经典面试真题解析

TIP

深入浅出互联网大厂 ES6 中 Map 高频面试真题解析 和 相关扩展知识

面试真题是检验自己学习成果和查缺补漏的最好方式之一,同时也是了解企业对求职者技能要求的风向标 。

# 1、Object 和 Map 的区别(小米、滴滴)

TIP

Object 和 Map 相似处在于,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。

不过 Map 与 Object 还是有一很重要的区别,下列情况中使有 Map 会是更好的选择。

点击查看,MDN 官方参考资料 (opens new window)

/ Map Object
意外的键 Map 默认情况不包含任何键。只包含显式插入的键。 一个 Object 有一个原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
键的类型 一个 Map 的键可以是任意值,包括函数、对象或任意基本类型。 一个 Object 的键必须是一个StringSymbol类型
键的顺序 Map 中的键是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。 虽然 Object 的键目前是有序的,但并不总是这样,而且这个顺序是复杂的。因此,最好不要依赖属性的顺序。
size Map 的键值对个数可以轻易地通过 size 属性获取。 Object 的键值对个数只能手动计算
迭代 Map 是 可迭代的 的,所以可以直接被迭代。 Object 没有实现迭代协议,所以使用 JavaSctipt 的 for...of表达式并不能直接迭代对象。
更多参考上面提供的 MDN 官方参考资 料
性能 在频繁增删键值对的场景下表现更好 在频繁添加和删除键值对的场景下未作出优化。
序列化和解析 没有元素的序列化和解析的支持。 原生的由 Object (opens new window) 到 JSON 的序列化支持,使用 JSON.stringify()
原生的由 JSON 到 Object的解析支持,使用 JSON.parse()
  • 意外的键,对象原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
function Point() {}
Point.prototype.x = 3;
const point = new Point();
// 自己定义的键名和原型上的键名产生冲突
point.x = 1;
  • 迭代,可以动为对象实现迭代协议,也可以用Object.keysObject.values方法
const obj = {
  a: 1,
  b: 2,
  c: 3,
};
for (let key of Object.keys(obj)) {
  console.log(key);
}

for (let key of Object.values(obj)) {
  console.log(key);
}
  • 序列化和反序列化

序列化: 把对象转化为可传输的字节序列过程称为序列化。
> 反序列化(解析): 把字节序列还原为对象的过程称为反序列化。

const obj = {
  username: "清心",
  age: 33,
};
// 将对象转换为字符串 (对象序列化)
const strJson = JSON.stringify(obj);
console.log(strJson);
console.log(typeof strJson);

// 将JSON字符串转换为对象,字符串解析
const obj2 = JSON.parse(strJson);
console.log(obj2);
console.log(typeof obj2);

为什么要序列化 ?

  • 其实序列化最终的目的是为了对象可以跨平台存储和进行网络传输。而我们进行跨平台存储和网络传输的方式就是 IO,而我们的 IO 支持的数据格式就是字节数组。
  • 因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则 (序列化),那么我们从 IO 流里面读出数据的时候再以这种规则把对象还原回来 (反序列化)

如果我们要把一栋房子从一个地方运输到另一个地方去,序列化 就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化 就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程

# 2、Map 与对象之间的互转(小米、滴滴)

Map 转对象

如果 Map 的键是字符串或 Symbol 类型,可以无损的转为对象,但是如果有非字符串和 Symbol 类型的键,那这个键名会被转换成字符串,再作为对象的键名。

const map = new Map()
  .set([1, 2, 3], true)
  .set(Symbol(), "符号")
  .set("username", "清心")
  .set({ a: 1 }, "对象");

// 将map转换为对象
function objToMap(map) {
  const obj = new Object();
  for (let [key, value] of map) {
    obj[key] = value;
  }
  return obj;
}
console.log(objToMap(map));

// {1,2,3: true, username: '清心', [object Object]: '对象', Symbol(): '符号'}

Map 转对象除了对键名有影响,还会影响成员的顺序

对象转 Map

对象转Map可以通过Object.entries()方法。

const obj = {
  a: 1,
  b: 2,
  c: 3,
};
// Object.entries(obj)方法,将对象转换成二维数组,数组的每个成员是一个双元素的数组
console.log(Object.entries(obj)); // [['a', 1],['b', 2],['c', 3]]
const obj = {
  a: 1,
  b: 2,
  c: 3,
};
const map = new Map(Object.entries(obj));
console.log(map); // Map(3) {'a' => 1, 'b' => 2, 'c' => 3}

# 七、Map 在实际开发中的应用

TIP

Map 和对象最大的区别在于其键可以是任意的类型,而对象的键只能是字符串和 Symbol 类型。

Map 常用来保存 DOM 节点和及节点相关的信息。

# 1、DOM 节点与 CSS 样式的映射关系

<p>111</p>
<p>222</p>
<p>333</p>
<p>444</p>
<p>555</p>

<script>
  const [p1, p2, p3, p4, p5] = document.querySelectorAll("p");

  // 单个添加 Map 成员
  //   const map = new Map();
  //   map.set(p1, "red");
  //   map.set(p2, "yellow");
  //   map.set(p3, "blue");
  //   map.set(p4, "green");
  //   map.set(p5, "skyblue");
  //   console.log(map); // Map(5) {p => 'red', p => 'yellow', p => 'blue', p => 'green', p => 'skyblue'}

  // 通过二维数组的方式添加Map成员
  const map = new Map([
    [p1, "red"],
    [p2, "yellow"],
    [p3, "blue"],
    [p4, "green"],
    [p5, "skyblue"],
  ]);

  //   map.forEach(function (color, ele) {
  //     ele.style.color = color;
  //   });

  // 如果不需要改变this指向,可以使用箭头函数,语法会更简洁
  map.forEach((color, ele) => {
    ele.style.color = color;
  });

  console.log(map);
</script>

image-20221025021028900

# 2、全屏加载动画

GIF2023-1-2022-30-52

其动画效果引用 animate.css,官方网站地址:https://animate.style/ (opens new window)

<!-- 导入animate.css库 -->
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
<style>
  html,
  body {
    margin: 0;
    padding: 0;
  }
  .container {
    width: 100%;
    height: 500px;
    background-color: rgb(233, 245, 250);
    position: relative;
    overflow: hidden;
  }
  .box {
    position: absolute;
  }
  .box1 {
    width: 200px;
    height: 100px;
    background-color: rgb(173, 252, 180);
    left: 200px;
    top: 40px;
  }
  .box2 {
    width: 400px;
    height: 200px;
    background-color: skyblue;
    left: 500px;
    top: 250px;
    left: 410px;
  }

  .box3 {
    width: 400px;
    height: 200px;
    background-color: rgb(243, 174, 191);
    left: 410px;
    top: 40px;
  }
  .box4 {
    width: 200px;
    height: 300px;
    background-color: khaki;
    left: 200px;
    top: 150px;
  }
</style>
<div class="container">
  <div class="box box1"></div>
  <div class="box box2"></div>
  <div class="box box3"></div>
  <div class="box box4"></div>
</div>

<script>
  // 利用解构赋值,将获取的DOM元素赋值给到变量
  let [box1, box2, box3, box4] = document.querySelectorAll(".container div");
  // 利用Map来建立DOM元素与其Class样式间的映射关系
  const map = new Map([
    [
      box1,
      ["animate__animated", "animate__backInDown", "animate__backOutDown"],
    ],
    [
      box2,
      ["animate__animated", "animate__backInLeft", "animate__backOutLeft"],
    ],
    [
      box3,
      ["animate__animated", "animate__backInRight", "animate__backOutRight"],
    ],
    [box4, ["animate__animated", "animate__backInUp", "animate__backOutUp"]],
  ]);

  // 利用for...of来遍历map,给DOM元素添加对应的Class样式
  for (let [k, v] of map) {
    k.classList.add(v[0]);
    k.classList.add(v[1]);
  }

  // 定时器在5分钟后执行
  setTimeout(function () {
    // 利用for...of来遍历map,给DOM元素添加和移除对应的Class样式
    for (let [k, v] of map) {
      k.classList.remove(v[1]);
      k.classList.add(v[2]);
    }
  }, 5000);
</script>

# 八、WeakMap 数据结构

TIP

WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。

不过 WeakMap 与 Map 还有两个重要的区别:

  • WeakMap只接通受对象类型作为键名,不接受其他类型的值作为键名。
  • WeakMap中键名所指向的对象保持弱引用,如果键所指对象没有其他地方引用他,就会被 GC 垃圾回收掉。

# 1、WeakMap 的基本用法

TIP

WeakMap 是一个构造函数,所以可以调用构造函数来创建 WeakMap 数据结构

  • 创建后,调用 set 方法添加成员
const obj1 = { a: 1 };
const obj2 = [1, 2, 3];
const wm = new WeakMap();
wm.set(obj1, "对象");
wm.set(obj2, "数组");
console.log(wm);
  • 创建同时初始化成员,其构造函数的参数必需是一个可迭代对象,可迭代对象返回的每一个成员是一个双元数组,数组的第一个元素必需是一个对象类型
const obj1 = { a: 1 };
const obj2 = [1, 2, 3];
const wm = new WeakMap([
  [obj1, "对象"],
  [obj2, "数组"],
]);
console.log(wm);

# 2、WeakMap 中成员特性

TIP

  • WeakMap 中的键名是唯一的,如果出现相同的键名,则后面的会覆盖前面的
  • WeakMap 的键名只能是一个对象类型,不能是其它类型。(2021 的新提案中,建议允许 Symbol 类型,但目前还不支持)
const obj1 = { a: 1 };
const obj2 = [1, 2, 3];
const arr1 = [1];
const arr2 = [1];
const wm = new WeakMap([
  [obj1, "对象"],
  [obj2, "数组"],
  [obj2, "数组2"],
  [arr1, "1"],
  [arr2, "2"],
]);
console.log(wm);

image-20230120164931013

# 3、WeakMap 实例的方法

TIP

WeakMap 只有以下 4 个方法

方法 说明
set(key,value) 设置WeakMap对应的键值,并返回当前WeakMap对象,所以set方法支持链式调用
get(key) 根据对应的键名,来获取对应的键值,没有对应的键名,返回undefined
has(key) 判断WeakMap中是否包含指定的键名,有返回true,没有返回false
delete(key) 根据指定的键名,从WeakMap中删除对应的键值对成员,删除成功返回true,否则返回false
const obj1 = { a: 1 };
const obj2 = [1, 2, 3];
const arr1 = [1];
const arr2 = [1];
const wm = new WeakMap();
// set添加成员
wm.set(obj1, "obj1值")
  .set(obj2, "obj2值")
  .set(arr1, "arr1值")
  .set(arr2, "arr2值");
console.log(wm); // WeakMap {Array(3) => 'obj2值', Array(1) => 'arr2值', {…} => 'obj1值', Array(1) => 'arr1值'}
// get获取成员值
console.log(wm.get(obj1)); // obj1值
console.log(wm.get({ a: 1 })); // undefined
// has判断是否有某个成员
console.log(wm.has(obj2)); // true
console.log(wm.has([1, 2, 3])); // false
// delete删除某个成员
console.log(wm.delete(obj2)); // true
console.log(wm.delete(arr2)); // true
console.log(wm); // WeakMap {{…} => 'obj1值', Array(1) => 'arr1值'}

# 4、WeakMap 中键名所指向的对象保持弱引用

TIP

所谓的弱引用,即垃圾回收不会将该引用考虑在内。只要该对象没有被其他引用,则垃圾回收机制就会回收该对象所占用的内存。

let obj1 = { a: 1, b: 2 }; // 对象{ a: 1, b: 2 } 被obj1引用+1
const wm = new WeakMap();
wm.set(obj1, "对象"); // 对象{ a: 1, b: 2 } 为弱引用,不会计数,则引用次数+0,总引用次数为1
obj1 = null; // 切断obj1对 对象{ a: 1, b: 2 }的引用,引用次数-1,总引用次数为0,垃圾回收回收该对象占用的内存
console.log(wm);

image-20230120171708726

温馨提示:

WeakMap 只针对键名的引用是弱引用,值的引用依然是强引用

let obj1 = { a: 1, b: 2 }; // 对象{ a: 1, b: 2 } 被obj1引用+1
let arr = [1, 2];
const wm = new WeakMap();
// arr是弱引用,垃圾回收不会将该引用计入在内,但obj1是强引用,垃圾回收会将该引用计入在内
wm.set(arr, obj1);
obj1 = null; // 切断obj2对对象{ a: 1, b: 2 }的引用,但obj1还被wm引用,所以引用次数为1,并不会被垃圾回收器回收
console.log(wm);

注:

因为 WeakMap 中对键名是弱引用,所以 WeakMap 没有size属性,也没有keys、values、entries、forEach方法,也不能通过 for...of 来遍历。

因为 WeakMap 中的成员随时有可能消失,被当成垃圾回收掉。

# 5、Map 与 WeakMap 的区别 ?面试真题解析

TIP

Map 和 WeakMap 是两种数据结构,用于操作键值之间的关系。

其都有get、set、delete、has方法,可以对成员做增删改查操作,不过他们存在以下不同点

区别 说明
键的类型 Map中的键可以是任意的当数据类型
WeakMap中的键只能是对象类型
键的引用 Map以中键指向的对象为强引用
WeakMap中键指向的对象为弱引用
属性 Mapsize属性,而WeakMap没有
方法 Mapkeys,values,entries,forEach方法,可以对其成员进行遍历
WeakMap没有keys,values,entries,forEach方法,其成员也不能进行遍历

# 九、WeakMap 在实际开发中的应用

TIP

当我们需要保存某个对象的相关信息,但又不想干扰垃圾回收机制对该对象的回收。我们就可以把这个对象与之相关的信息以键值对的形式保存在 WeakMap 中。

WeakMap 常用来存储一些只有当前对象存在时才有用的信息,如果对象不存在了,这些信息也就没有了,应当和对象一起被垃圾回收掉。

# 1、缓存数据

TIP

当某个函数的计算结果需要被记住”缓存“时,我们就可以把对象作为键,其相关信息做为值,添加到 WeakMap 中,在后续我们需要用到相关信息时,如果缓存中有,就直接读取缓存的,如果没有,则重新存一份到 WeakMap 中。

这样,只要对象被销毁,其相关的信息也就被销毁,而不会造成内存泄露。

以上提到的缓存,并非指浏览器的缓存,而是指对象的某个信息需要经过大量的计算才能得到,但每次计算的结果又相同,这时我们可以把这个计算的结果保存起来,后面需要用时,直接拿来用。

let cache = new WeakMap(); // 用来保存对象需要缓存的信息
function getIdentity(obj) {
  // 判断缓存中是否有缓存该对象内容,没有就计算一次,然后将计算的结果缓存起来
  if (!cache.has(obj)) {
    console.log("没有走缓存");
    let identity;
    if (obj.age < 18) {
      identity = "少年";
    } else if (obj.age < 38) {
      identity = "青年";
    } else if (obj.age < 48) {
      identity = "中年";
    } else {
      identity = "老年";
    }
    // 加入到cache中缓存起来
    cache.set(obj, identity);
  }
  // 最后返回缓存中的结果
  return cache.get(obj);
}

let obj = {
  username: "清心",
  age: 33,
};
console.log(getIdentity(obj));
console.log(getIdentity(obj));
obj = null;
// obj=null,切断obj与{username: "清心",age: 33}对象的引用,obj的引用次数为0,垃圾回收会回收掉对象占用的内存
// 因为cache中对对象的引用为弱引用,垃圾回收机制不会考虑

注:

如果上面的结果不保存在 WeakMap 中,而保存 Map 中,那 obj = null 时,垃圾回收机制并不认为对象是垃圾,因为对象在 Map 还在引用着。所以不手动清除 Map 中对对象的引用,就会造成内存泄露。

你可能会说,那我可以把返回的结果保存在一个变量中,后面需要用到,直接用变量中的结果就可以,但这样我们在设置 obj = null 时,对象会当成垃圾回收,但变量保存的结果占用着内存,也会造成内存泄露。

# 2、模拟私有变量

TIP

把对象与当前对象的私有属性形成键值对的映射关系,存入到 WeakMap 中,这样创建出来的实例对象身上没有这个私有属性,也就能真正达到属性私有化的目的。

const Stack = (function () {
  const privates = new WeakMap(); // 用来保存私有属性
  function Stack() {
    privates.set(this, []); // 模拟对象的私有属性,属性对应值为数组
  }
  // 入栈
  Stack.prototype.push = function (value) {
    privates.get(this).push(value);
    return this; // 返回值为调用当前方法的对象,支持链式调用
  };
  // 出栈
  Stack.prototype.pop = function () {
    return privates.get(this).pop();
  };
  // 查看栈元素
  Stack.prototype.view = function () {
    console.log(privates.get(this));
  };

  return Stack;
})();

// 创建一个栈
const stack = new Stack();
stack.push(1).push(2).push(3);
console.log(stack.pop()); // 3
stack.view(); //  [1, 2]

# 十、重难点总结

TIP

总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。

用于故而知新,快速复习。

# 1、Set 是什么 ?

TIP

  • Set 中的成员是唯一的,没有重复值,这一特点,我们可以用它来做数组或字符串的去重。
  • Set 中成员的遍历顺序和成员插入时的顺序是一致的。
  • Set 没有下标序号,即没有办法通过指定的序号来访问特定的成员

# 2、Map 是什么 ?

TIP

  • Map 的本质是键值对的集合(和对象一样本质都是键值对的集合)
  • 对象与 Map 不同的是,对象只能使用字符串和 Symbol 来当做键,而 Map 任意数据类型都可以作为键

# 3、Set 和 Map 实例的方法与属性

方法与属性 Set Map
添加成员的方法 add() set()添加成员,get()获取指定的成员
判断是否拥有某个成员 has() has()
删除某个值,清除所有成员 delete()clear() delete()clear()
遍历所有成员 forEach() forEach()
成员总数 size 属性 size 属性

注:

我们可以看到,Set 和 Map 实例的方法和属性除了 添加成员的方法不同之外,其他都一样。

# 4、Set 和 Map 构造函数的参数

TIP

  • Set:数组、字符串、arguments、NodeList、Set 等,其实参数只要是一个可迭代对象就行
  • Map:数组(二维数组),其实参数只要是一个可迭代对象,同时迭代对象返回的每一个成员是一个二元数组就可以

注:其中我们用到最多是数组的形式,其他了解知道就行。

# 5、Set 和 Map 对相同值/键的判断

TIP

  • 基本可用严格相等 === 判断
  • 例外:对于 NaN 的判断与===不同,Set/Map 中 NaN 等于 NaN

# 6、什么时候使用 Set ?

TIP

  • 数组 或 字符串去重时
  • 不需要通过下标访问,只需要遍历时
  • 为了使用 Set 提供的方法和属性时

以上 2、3 两条使用数组或 Set 都可以,根据实际情况而定,没有严格的标准。

# 7、什么时候使用 Map ?

TIP

  • 只需要 key -> value 的结构时
  • 需要字符串以外的值做键时
  • 为了使用 Map 提供的方法和属性时

当然,以上第 1、3 条使用对象或 Map 都可以,根据实际情况来就 OK,如果觉得对象字面量更直观就使用对象。

# 8、关于 WeakSet 与 WeakMap

TIP

WeakSet 中的成员只能是对象,不能是其它类型。WeakMap 中的键名只能是对象类型,不能是其它类型

WeakSet 中成员的引用为弱引用,WeakMap 中键指向的对象也属于弱引用。

  • 当我们需要存储一个对象,而以不希望这个对象干扰垃圾回收机制时,就可以用 WeakSet
  • 当我们需要存储一个对象及对象相关的信息,而不希望这个对象干扰垃圾回收机制时,就可以用 WeakMap。

WeakSet 和 WeakMap 中的成员都是弱引用,所以都没有 size、forEach、keys、values、entries 方法,也不能通过 for...of 来遍历

# 9、实际应用场景

TIP

手动实现 Set、Map、WeakSet、WeakMap 在实际开发中的应用相关的案例。

# 十一、测试题

TIP

自我测试:在不看答案的前提下,看看自己是否真正掌握了本节所学内容。

# 1、下列关于 Set 的描述,错误的选项是 ?

选择两项

  • A、Set 中的值必须是唯一的
  • B、可以通过下标的方法访问 Set 中的值
  • C、Set 可以结合...扩展运算符,在数组中展开
  • D、Set 中 NaN 与 NaN 是相等的
自己先分析,再点击查看正确答案

正确答案:B

# 2、以下代码中,输入的 this 值是 ?

选择一项

let s = new Set();
s.add(1).add(2).add(2);
const obj = {};
s.forEach(() => {
  console.log(this);
}, obj);
  • A、window
  • B、undefined
  • C、obj
  • D、s
自己先分析,再点击查看正确答案

正确答案:A

# 3、以下代码中,会报错的选项是 ?

选择一项

  • A、
const s = new Set("icoding");
console.log(s);
  • B、
const s = new Set(3);
console.log(s);
  • C、
const s = new Set([1, 2]);
const s2 = new Set(s);
console.log(s2);
  • D、
function fn() {
  const s = new Set(arguments);
  console.log(s);
}
fn();
自己先分析,再点击查看正确答案

正确答案: B

# 4、Map 和 Set 实例共有的方法和属性是 ?

选择二项

  • A、add
  • B、size
  • C、forEach
  • D、get
自己先分析,再点击查看正确答案

正确答案:B、C

# 5、关于 Map 对象,以下说法不正确的是 ?

选择一项

  • A、Map 是映射的意思,Set 是集合的意思
  • B、Map 构造函数中的参数,可以是任意的可迭代对象
  • C、通过 size 属性,可以获取 Map 成员的个数
  • D、Map 可以结合 ... 展开运算符,在数组中展开
自己先分析,再点击查看正确答案

正确答案:B

# 十二、面试题

TIP

深入浅出互联网大厂 ES6 高频面试真题解析 和 相关扩展知识

面试真题是检验自己学习成果和查缺补漏的最好方式之一,同时也是了解企业对求职者技能要求的风向标 。

# 1、 实现函数的链式调用(商汤)

TIP

写一个类,其构造出来的对象打点调用自身的方法,这个方法支持链式调用。

方法支持链式调用的本质:该方法返回值为调用该方法的对象本身

const privates = new WeakMap(); // 创建map用来保存私有属性
function Stack() {
  privates.set(this, []);
}
Stack.prototype.add = function (value) {
  privates.get(this).push(value);
  return this; // 返回值为当前实例对象本身
};
Stack.prototype.view = function () {
  console.log(privates.get(this));
};

const stack = new Stack();
stack.add(1).add(2).add(3);
console.log(stack.view());

# 2、 利用 map 记录字符串中每个字符出现次数(小米)

TIP

比如:字符串:“abcdaaasscccdeeesdd“ 中每个字符出现的次数,分别为

a=>4,b=>1,c=>4,d=>4,s=>3,e=>3

解题思路

利用 for...of 遍历字符串,把字符串中的元素和元素出现的次数,以键值对的形式添加到Map中。 最后输出map

let str = "abcdaaasscccdeeesdd";
// 创建Map
const map = new Map();
for (let k of str) {
  // 如果存在k,则取出k对应值然后+1,如果不存在,则其值为0+1=1
  map.set(k, (map.get(k) || 0) + 1);
}
console.log(map);

# 3、找数组中的主要元素

TIP

所谓的数组中的主要元素是指,数组中的某个元素有出现次数超过了数组长度的一半,那这个元素为数组中的主要元素,返回当前元素。如果没有主要元素,返回值为 -1

如:

  • 数组[1,2,4,4,4,4,5]中主要元素为 4,返回值为 4。
  • 数组[1,2,4,4,4,5]中没有主要元素,返回值-1。

解题思路:

  • 利用用for...of遍历数组,把数组中的元素和元素出现的次数,以键值对的形式添加到 Map 中
  • 遍历 Map,找出 Map 中键对应的值 > 数组长度 / 2 的元素
  • 如果没有找到,则返回 -1,如果找到,返回对应的元素
// 找出数组中的主要元素
function findMajorElement(arr) {
  // 创建Map,用来统计数组中每个元素出现的次数
  const map = new Map();
  // 遍历数组,把数组中每个元素及出现的次数以键值对形式存入map中
  for (let k of arr) {
    map.set(k, (map.get(k) || 0) + 1);
  }
  let result; // 用来保存找到的元素
  // 遍历map,找出出现次数>数组长度/2的元素
  for (let [k, v] of map) {
    if (v > arr.length / 2) result = k;
  }
  // 判断result是否有值,如果有值返回对应值,如果没有则返回-1
  return result === undefined ? -1 : result;
}

console.log(findMajorElement([1, 2, 3, 3, 3, 5])); //-1
console.log(findMajorElement([1, 2, 3, 3, 3, 5, 3])); // 3
console.log(findMajorElement([1, 2, 3, 3, 3, 2, 2, 2, 2])); // 2
上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X