一起养成写作习惯!这是我参与「掘金日新计划 · 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 迭代器的构造函数
具体🌰
// 迭代整个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”。
另外,NaN和undefined都可以被存储在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);
总结横向对比
下面我们总结一下他们的差异
| Set | WeakSet | |
|---|---|---|
| 参数 | 数组,字符串,数字,undefined,对象,NaN,布尔值 | 对象,多维数组 |
| 用法 | add,delete,has,clear,size | add,delete,has |
| 是否能枚举 | √ | × |
| 应用场景 | 数组去重 | 检查对象是否循环引用 |
小结
这篇文章到这里就结束了,水平有限难免有纰漏,欢迎纠错。最后希望帮忙点点赞,这对我创作是无比的肯定和动力。希望可以帮到你,写的时间真的远远不止 5分钟,下一期我们来聊聊 Map 和 WeakMap
文章参考
es6.ruanyifeng.com/#docs/set-m…
developer.mozilla.org/zh-CN/docs/…