前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

ES6 Set、WeakSet、Map、WeakMap 用法与区别详解

首页2019-09-22 15:50:43Front-End
JavaScriptES6数据结构WeakMap

数组去重写 [...new Set(arr)],这个大家都会。但再往下问一层就容易卡壳:为什么 Set 里 NaN 只会存一个,而 === 明明说 NaN !== NaN?给 DOM 节点挂一份额外数据,用 WeakMap 和用普通对象到底差在哪?Map 和普通对象都是键值对,什么时候该换成 Map?

这篇把这四个结构一次讲清楚,每个都给出判等规则、方法清单、转换手法和适用场景,最后补一节这几年新增的能力。文中的结论我在 Node 22.23.1 上逐条验证过。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • Set 的判等规则 SameValueZero,以及它和严格相等的两处差别
  • Set 的实例方法清单,交集并集差集的手写实现
  • WeakSet 和 WeakMap 的弱引用到底弱在哪,为什么它们不能遍历
  • Map 的键为什么是跟内存地址绑定的,以及和 Object 的性能取舍
  • Map 与 Array、Object、JSON 之间的六种转换写法
  • 这几年新增的集合运算方法、分组方法和 WeakRef

Set 和 Map 主要的应用场景在于数据重组和数据储存。Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构。

# 一、集合(Set)

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

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

new Set([iterable])
@前端进阶之旅: 代码已经复制到剪贴板

举个例子:

const s = new Set();
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x));

for (let i of s) {
    console.log(i)	// 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1];
console.log([... new Set(arr)])	// [1, 2, 3]
@前端进阶之旅: 代码已经复制到剪贴板

这里有个坑要注意,原文这两段代码是没有分号的,直接跑会报错。原因是自动分号插入不会在行尾是 ) 而下一行以 [ 开头时补分号,const s = new Set() 换行接 [1, 2, 3...] 会被解析成 new Set()[1, 2, 3, 4, 3, 2, 1],也就是取属性而不是新建数组。这类问题在不写分号的代码风格里挺常见,行首是 [、(、`、+、- 这五种字符时都要当心。上面的代码我补上了分号。

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

# 1.1 判等规则 SameValueZero

向 Set 加入值的时候不会发生类型转换,所以 5 和 "5" 是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做「SameValueZero」,它类似于精确相等运算符(===),主要的区别是 Set 认为 NaN 等于自身,而精确相等运算符认为 NaN 不等于自身。

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1])	// [5, "5"]
@前端进阶之旅: 代码已经复制到剪贴板

把这条规则说完整一点,JavaScript 里一共有四套判等:

算法 对应写法 NaN 与 NaN +0 与 -0
宽松相等 == 不等 相等
严格相等 === 不等 相等
SameValueZero Set / Map / includes 相等 相等
SameValue Object.is 相等 不等

Set、Map、Array.prototype.includes 走的都是 SameValueZero,所以 [NaN].includes(NaN) 是 true 而 [NaN].indexOf(NaN) 是 -1。这个差别偶尔会咬人。

对象引用不参与值比较,两个内容一样的对象在 Set 里是两个成员。想按内容去重,得先序列化成字符串再去重,或者自己写一轮比较。

# 1.2 实例属性

  • constructor:构造函数
  • size:元素数量
let set = new Set([1, 2, 3, 2, 1])

console.log(set.length)	// undefined
console.log(set.size)	// 3
@前端进阶之旅: 代码已经复制到剪贴板

注意是 size 不是 length。这个设计上和 Map 保持一致,也和数组区分开了,因为 Set 没有下标的概念。

# 1.3 操作方法

  • add(value):新增,相当于 array 里的 push
  • delete(value):存在即删除集合中 value
  • has(value):判断集合中是否存在 value
  • clear():清空集合
let set = new Set()
set.add(1).add(2).add(1)

set.has(1)	// true
set.has(3)	// false
set.delete(1)
set.has(1)	// false
@前端进阶之旅: 代码已经复制到剪贴板

add 返回的是 Set 本身,所以可以链式调用;delete 返回布尔值表示有没有删掉东西。这两个返回值的差别记一下,写链式的时候容易搞混。

Array.from 方法可以将 Set 结构转为数组:

const items = new Set([1, 2, 3, 2])
const array = Array.from(items)
console.log(array)	// [1, 2, 3]
// 或
const arr = [...items]
console.log(arr)	// [1, 2, 3]
@前端进阶之旅: 代码已经复制到剪贴板

has 是 Set 最有价值的地方。数组的 includes 是线性查找,数据量一大就慢;Set 的 has 是基于哈希的,接近常数时间。判重、白名单校验、已访问节点标记这类场景,用 Set 替掉数组能带来数量级的差别。

# 1.4 遍历方法

遍历顺序为插入顺序,这一点是规范保证的,不是实现细节。

  • keys():返回一个包含集合中所有键的迭代器
  • values():返回一个包含集合中所有值的迭代器
  • entries():返回一个包含 Set 对象中所有元素的键值对迭代器
  • forEach(callbackFn, thisArg):用于对集合成员执行 callbackFn 操作,如果提供了 thisArg 参数,回调中的 this 会是这个参数,没有返回值
let set = new Set([1, 2, 3])
console.log(set.keys())	// SetIterator {1, 2, 3}
console.log(set.values())	// SetIterator {1, 2, 3}
console.log(set.entries())	// SetIterator {1, 2, 3}

for (let item of set.keys()) {
  console.log(item);
}	// 1	2	 3
for (let item of set.entries()) {
  console.log(item);
}	// [1, 1]	[2, 2]	[3, 3]

set.forEach((value, key) =>  {
    console.log(key + ' : ' + value)
})	// 1 : 1	2 : 2		3 : 3
@前端进阶之旅: 代码已经复制到剪贴板

Set 里 keys() 和 values() 返回的是一样的东西,因为集合只有值没有键。entries() 给出的是 [value, value] 这种成对形式,纯粹是为了和 Map 的接口保持一致,实际很少用。

Set 可默认遍历,默认迭代器生成函数是 values() 方法:

Set.prototype[Symbol.iterator] === Set.prototype.values	// true
@前端进阶之旅: 代码已经复制到剪贴板

我在 Node 22 上确认过这一行返回 true。这就是 [...set] 和 for...of 能直接用在 Set 上的原因,展开运算符找的就是 Symbol.iterator。

Set 本身没有 map 和 filter,但转成数组之后就有了:

let set = new Set([1, 2, 3])
set = new Set([...set].map(item => item * 2))
console.log([...set])	// [2, 4, 6]

set = new Set([...set].filter(item => (item >= 4)))
console.log([...set])	//[4, 6]
@前端进阶之旅: 代码已经复制到剪贴板

因此,Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference):

let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])

let intersect = new Set([...set1].filter(value => set2.has(value)))
let union = new Set([...set1, ...set2])
let difference = new Set([...set1].filter(value => !set2.has(value)))

console.log(intersect)	// Set {2, 3}
console.log(union)		// Set {1, 2, 3, 4}
console.log(difference)	// Set {1}
@前端进阶之旅: 代码已经复制到剪贴板

这三行是 2019 年的标准写法,现在有原生方法了,第七节会讲。

# 二、WeakSet

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

WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放原始值,而 Set 两者都可以
  • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的引用。如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中)。所以 WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后有的成员可能取不到了。ES6 规定 WeakSet 不可遍历,也没有办法拿到它包含的所有元素

那为什么规范干脆禁掉遍历,而不是让你自己承担风险呢?

因为一旦允许遍历,垃圾回收的时机就变成了可观测的行为。同一段代码在不同引擎、不同内存压力下遍历出的结果会不一样,这在语言层面是不可接受的。禁掉遍历,弱引用就成了一个纯粹的内部实现细节。同理,WeakSet 也没有 size 属性。

属性:

  • constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数
const arr = [[1, 2], [3, 4]]
const weakset = new WeakSet(arr)
console.log(weakset)
@前端进阶之旅: 代码已经复制到剪贴板
2019-03-08 9 24 34
fe
  • 一、集合(Set)
    • 1.1 判等规则 SameValueZero
    • 1.2 实例属性
    • 1.3 操作方法
    • 1.4 遍历方法
  • 二、WeakSet
  • 三、字典(Map)
    • 3.1 键是跟内存地址绑定的
    • 3.2 属性和方法
    • 3.3 与其他数据结构的相互转换
  • 四、WeakMap
  • 五、四种结构怎么选
  • 六、扩展:Object 与 Set、Map
  • 七、这几年新增的能力
  • 总结
  • 参考

← React Native真机调试完整流程与连不上的排查顺序彻底弄懂 JavaScript 执行机制,Event Loop 宏任务与微任务 →