五分钟面试题—— 为啥 Set 可以去重呢?

250 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前言 & 背景

我们在面试过程中,被经常问到的话术往往是这样的

  • ES6 中用过哪些方法,比较常用有哪些?
  • 有没有了解过 ES6 提供了新的数据结构 Set 和 WeakSet
  • 它们之间有什么区别呢(回答的时候可以涉及的范围很大,包括用法,语法,底层原型属性,应用场景等)
  • 为什么 Set 可以去重呢,有了解原理吗?
  • 有了解 ES2021 的 WeakRef 吗?

一套话术下来,由浅入深,在一步步深入的过程中,我们在脑海里面模拟一次,面试场景就会发现我们的知识盲区和漏洞,那么我们不妨稍稍总结一下,面试的时候考察点

  • 技术基础(可以理解为广度),对基础常用的功能的了解和熟练程度
  • 技术深度,对基础常用的语法功能,有没有了解背后的原理和核心细节
  • 对新技术有敏感性, 能够主动针对实际业务自学新的技术,拓宽自己的技术边界

一层层的对自己发问,填坑,相信对自己技术水平提高是很好的帮助!下面我们正式开始

如果刚好是自己的知识盲区不妨帮忙点赞支持一下

背景

最近在面试候选人的过程中,往往遇到很难从简历中获取到他擅长或者熟悉部分,进行深度沟通交流,所以不得不问一些 八股文 基础,看看候选人对一个简单点,理解的深度。

Set

Set对象是值的集合,你可以按照插入的顺序迭代它的元素。 Set中的元素只会出现一次,即 Set 中的元素是唯一的。

Set 实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。 实例属性

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

迭代 Set

Set 可以使用迭代器进行迭代,是因为存在 Symbol.iterator 迭代器的构造函数

image.png

具体🌰

// 迭代整个set
// 按顺序输出:1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}
for (let item of mySet) console.log(item);

// 按顺序输出:1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}
for (let item of mySet.keys()) console.log(item);

// 按顺序输出:1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}
for (let item of mySet.values()) console.log(item);

// 按顺序输出:1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}
//(键与值相等)
for (let [key, value] of mySet.entries()) console.log(key);

// 使用 Array.from 转换Set为Array
var myArr = Array.from(mySet); // [1, "some text", {"a": 1, "b": 2}, {"a": 1, "b": 2}]

// 如果在HTML文档中工作,也可以:
mySet.add(document.body);
mySet.has(document.querySelector("body")); // true

// Set 和 Array互换
mySet2 = new Set([1, 2, 3, 4]);
mySet2.size;               // 4
[...mySet2];               // [1,2,3,4]

// 可以通过如下代码模拟求交集
let intersection = new Set([...set1].filter(x => set2.has(x)));

// 可以通过如下代码模拟求差集
let difference = new Set([...set1].filter(x => !set2.has(x)));

// 用forEach迭代
mySet.forEach(function(value) {
  console.log(value); // 1 2 3 4
});

为啥 Set 可以去重呢?

因为 Set 中的值总是唯一的,所以需要判断两个值是否相等。在ECMAScript规范的早期版本中,这不是基于和===操作符中使用的算法相同的算法。具体来说,对于 Set s, +0 (+0 严格相等于-0)和-0是不同的值。然而,在 ECMAScript 2015规范中这点已被更改。有关详细信息,请参阅浏览器兼容性 表中的“Key equality for -0 and 0”。

另外,NaNundefined都可以被存储在Set 中, NaN之间被视为相同的值(NaN被认为是相同的,尽管 NaN !== NaN)。

var test = new Set();

test.add({name: 'a'}); // Set(1)

test.add({name: 'a'}); // Set(2) Set中存储了 两个对象,两个对象的存储的引用地址是不同的

在上面这个例子就可以清晰看出,Set 是对引用地址去重的,如果不同的引用地址的话,将不会未去重

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

  • WeakSet 的成员只能是对象,而不能是其他类型的值。
  • WeakSet持弱引用:集合中对象的引用为弱引用。 如果没有其他的对WeakSet中对象的引用,那么这些对象会被当成垃圾回收掉。 这也意味着WeakSet中没有存储当前对象的列表。 正因为这样,WeakSet 是不可枚举的。

WeakSet 弱引用问题

WeakSet 对象允许你将 弱保持对象 存储在一个集合中。

WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

这是因为垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。

由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。

这个特点同样也是适用于 WeakMap,顾名思义 Weak 就是弱的意思嘛

var a = {kobe: 123}
var b = new WeakSet();
b.add(a);
b.has(a); // true

a = null; // 释放内存
console.log(b); // WeakSet()

WeakSet 实例方法

WeakSet 结构有以下三个方法。

  • WeakSet.prototype.add(value) :向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value) :清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value) :返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
new WeakSet([iterable]);
 
var ws = new WeakSet();
var foo = {};
var bar = {};

ws.add(foo);
ws.add(bar);

ws.has(foo);    // true
ws.has(bar);   // true

ws.delete(foo); // 从set中删除 foo 对象
ws.has(foo);    // false, foo 对象已经被删除了
ws.has(bar);    // true, bar 依然存在

应用场景 检查是否循环引用

递归调用自身的函数需要一种通过跟踪哪些对象已被处理,来应对循环数据结构的方法。 为此,WeakSet非常适合处理这种情况:

// 对 传入的subject对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = null) {
    if(!_refs) _refs = new WeakSet();

    // 避免无限递归
    if(_refs.has(subject)) return;

    fn(subject);
    if("object" === typeof subject){
        _refs.add(subject);
        for (let key in subject) {
            execRecursively(fn, subject[key], _refs);
        }
    }
}

const foo = {
    foo: "Foo",
    bar: {
        bar: "Bar"
    }
};

foo.bar.baz = foo; // 循环引用!
execRecursively(obj => console.log(obj), foo);

总结横向对比

下面我们总结一下他们的差异

SetWeakSet
参数数组,字符串,数字,undefined,对象,NaN,布尔值对象,多维数组
用法add,delete,has,clear,sizeadd,delete,has
是否能枚举×
应用场景数组去重检查对象是否循环引用

小结

这篇文章到这里就结束了,水平有限难免有纰漏,欢迎纠错。最后希望帮忙点点赞,这对我创作是无比的肯定和动力。希望可以帮到你,写的时间真的远远不止 5分钟,下一期我们来聊聊 Map 和 WeakMap

文章参考

es6.ruanyifeng.com/#docs/set-m…
developer.mozilla.org/zh-CN/docs/…