第十一章 Set和Map数据结构
前面介绍完了7种数据类型,本章节阮一峰老师开始讲起了数据结构 Set
和 Map
。这两个数据结构在开发中十分常用,并且二者都有弱引用写法。
本章节着重从它们的含义、语法、用法搭配示例入手,尽可能地详细解释说明。
Set
Set 是 ES6 引入的一种新的数据结构,类似于数组,但其中的元素值唯一,不允许重复。
基本用法
创建
Set
实例:使用
new Set()
创建一个空的Set
实例。可以通过传入一个数组或其他可迭代对象来初始化 Set,初始化时会自动去除重复的元素。
jsconst s = new Set(); // 创建一个空的 `Set` 实例 const set1 = new Set([1, 2, 3, 4, 4]); // 初始化 Set,自动去重
向
Set
添加元素:使用
add
方法向Set
添加新元素,如果元素已经存在则不会重复添加。jsconst s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); // s 现在为 `Set` { 2, 3, 5, 4 }
遍历 Set:
使用
for...of
循环可以遍历Set
中的所有元素,遍历顺序即插入顺序。jsconst s = new Set([2, 3, 5, 4]); for (let i of s) { console.log(i); // 输出 2, 3, 5, 4 }
获取
Set
的大小:使用
size
属性可以获取Set
中元素的数量。jsconst items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); items.size; // 输出 5
特殊情况处理:
Set
内部使用Same-value equality
算法判断元素是否相等,类似于===
运算符,但是对NaN
的处理不同。在Set
中NaN
是等于自身的。jslet set = new Set(); set.add(NaN); set.add(NaN); set; // 输出 `Set` { NaN }
对象是根据引用地址判断是否相等,而非对象内容。
jslet set = new Set(); set.add({}); // 第一个空对象 set.add({}); // 第二个空对象,不同的引用 set.size; // 输出 2
Set实例的属性和方法
Set 拥有 2个属性和 4种方法:
- 属性
Set.prototype.constructor
:构造函数,默认是Set
函数Set.protptype.size
:返回Set
实例的成员数量
- 方法
add
:添加某个值,返回添加后的Set
结构delete
:删除某个值,返回布尔值表示是否删除成功has
:返回布尔值表示Set
内是否存在该参数clear
:清空 Set,无返回值
下面来使用一下:
const s = new Set()
s.add(1).add(2).add(2)
console.log(s, s.size) // [1, 2], 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2)
s.has(2) // false
Array.from
可以把 Set
转为数组,这就提供了数组去重的方法。
遍历操作
Set
有四种遍历方法:
keys()
返回键名values()
返回键值entries()
返回键值对对于
Set
而言,只有键值没有键名(或者说键名与键值相同),因此keys()
方法和values()
方法返回的值是一样的,entries()
方法返回的数组两个值一样。jslet set = new Set(['daodao', 'xiaodao', 'duyidao']) for (let key of set.keys()) { console.log(key) } // daodao // xiaodao // duyidao for (let value of set.values()) { console.log(value) } // daodao // xiaodao // duyidao for (let entrie of set.entries()) { console.log(entrie) } // [daodao, daodao] // [xiaodao, xiaodao] // [duyidao, duyidao]
另外
Set
默认可遍历,遍历返回的值默认是values()
。forEach()
使用回调函数遍历每个成员jslet set = new Set([1, 2, 3]) set.forEach((value, key) => { console.log(value) }) // 1 // 2 // 3
特别的,Set
插入顺序就是遍历的顺序,这点在做回调函数列表时非常有用,能够保证按照插入顺序依次执行。
WeakSet
含义
WeakSet
与 Set
一样,也是不重复值的集合,但是有两点区别:
成员对象。
WeakSet
的成员对象只能是对象,如果使用其他类型的值,会报错jslet wset = new WeakSet() wset.add(1) // TypeError: Invalid value used in weak set.
弱引用。
WeakSet
中的对象都是弱引用类型,若外部不再引用该对象,则垃圾回收器会自动回收,不考虑该对象是否还在WeakSet
中使用。因此,ES6 规定WeakSet
不可遍历
语法
由于 WeakSet
是一个构造函数,因此需要使用 new
创建数据结构。可以接收任何具有 iterable
接口的对象作为参数。
let a = new WeakSet([[1, 2], [3, 4]])
请注意,上述代码是每一项成员都是一个数组,只有数组对象可以作为 WeakSet
的成员。
let b = new WeakSet([1, 2]) // 报错
与 Set
相比,WeakSet
由于不能遍历,因此少了 size
等两种属性和 clear()
方法,只有三种方法:
add(value)
添加新成员delete(value)
删除指定成员has(value)
返回布尔值,表示是否有该值
Map
含义
ES6 之前都是使用对象 Object
做键值对的集合,但是对象有一个很大的限制,他只能使用字符串作为键名。
let element = document.querySelector('#content')
let obj = {}
obj[element] = 'content'
console.log(obj['[object HTMLDivElement]']) // content
上述代码是因为对象只能接受字符串,因此获取到的 DOM
元素会被 toString()
隐式转换为字符串。
ES6 新出了 Map
数据结构,类似于对象,但是键名不仅限于字符串,各种类型(包括对象)都可以作为键名。
Map
有两种添加值的方法:
set()
方法添加值,get()
方法获取值,delete
方法删除值,has()
方法判断是否有值jslet m = new Map() m.set([1, 2], 'array') m.get([1, 2]) // array m.has([1, 2]) // true
接收一个二维数组作为参数,第二维的数组第一项是键名,第二项是键值
jslet m = new Map([ ['name', '张三'], ['age', '24'] ]) m.size // 2 m.get('name') // 张三 m.has('age') // true
这个方法本质上是对传递的数组参数执行循环算法,依次
.set
赋值。jsconst items = [ ['name', '张三'], ['age', '24'] ] const map = new Map() items.forEach(([value, key]) => { map.set(key, value) })
不仅是数组,所有具有
Iterator
接口且每个成员都是双元素数组的数据结构都可以作为Map
的构造函数参数。因此,Set
和Map
都可以用来生成新的Map
。jslet set = new Set([ ['foo', 123], ['bar', 456] ]) let map = new Map(set) map.get('foo') // 123 map.get('bar') // 456 let m1 = new Map([ ['n', 1], ['m', 2] ]) let m2 = new Map(m1) m2.get('n') // 1 m2.get('n') // 2
⚠ 注意
对于基本数据类型,只要值相同即可(需要注意的是,0 和 -0 在 Map
中是相同的;NaN
不严格等于自身,但在 Map
中视为相同的)
// 基本数据类型
let mm = new Map()
mm.set(0, '0')
mm.get(-0) // '0'
mm.set(NaN, 123)
mm.get(NaN) // 123
对于数组和对象这类复杂数据类型,Map
结构将其内存地址作为是否同一个键的比较标准。
let map = new Map()
map.set(['a'], 1)
map.get(['a']) // undefined
let arr = ['b']
map.set(arr, 'array')
map.get(arr) // array
实例属性与语法
属性
size
属性可以获取Map
成员的总数jslet map = new Map([ ['a', 1], ['b', 2], ['c', 3] ]) map.size // 3
方法
set(key, value)
方法设置 key
所对应的键值对,并返回总的 Map
结构。可以采取链式写法。
const m = new Map()
.set(1, 'daodao')
.set('2', 'xiaodao')
.set(undefined, 'duyidao')
get(key)
方法获取 key
对应的键值,找不到返回 undefined
。
const m = new Map()
const sayHi = () => {
console.log('hello')
}
m.set('hi', sayHi)
m.get('hi') // function
m.get('hello') // undefined
has(key)
方法返回布尔值,表示该键值是否在 Map
中有数据。
delete(key)
方法删除 Map
中某个键,删除成功返回 true
,反之返回 false
。
clear()
方法清空所有成员,无返回值。
遍历操作
同 Set
一致,Map
也拥有 keys()
、values()
、entries()
和 forEach()
四种遍历方法,遍历的顺序也是插入顺序。
const map = new Map([
[1, 'a'],
[2, 'b'],
[3, 'c'],
])
for (let k of map.keys()) {
console.log(k)
}
// 1
// 2
// 3
for (let k of map.values()) {
console.log(k)
}
// a
// b
// c
for (let k of map.entries()) {
console.log(k[0], k[1])
}
// 1, a
// 2, b
// 3, c
Map
可以通过拓展运算符 ...
实现快速转数组操作,如转为数组、使用 map
或 filter
执行操作等。
let map = new Map().set(1, 'a').set(2, 'b').set(3, 'c')
let arr = [...map] // [[1, 'a'], [2, 'b'], [3, 'c']]
let keys = [...map.keys()] // [1, 2, 3]
let filterMap = new Map([...map].filter(([key, value]) => value !== 'c')) // Map(2) {1 => 'a', 2 => 'b'}
let mapMap = new Map([...map].map(([key, value]) => [key * 2, value + '-'])) // Map(3) {2 => 'a-', 4 => 'b-', 6 => 'c-'}
此外,Map
可以使用 forEach
方法实现遍历操作,使用方式和数组一致。
let map = new Map([
[1, 'a'],
[2, 'b']
])
map.forEach((value, key, item) => {
console.log(value, key, item) // a, 1, [1, 'a']
})
Map
可以接收第二个参数,用于绑定 this
。
let map = new Map([
[1, 'a'],
[2, 'b']
])
let ojb = {
sayLog: (v, k, e) => {
console.log(v, k, e)
}
}
map.forEach((value, key, item) => {
this.sayLog(value, key, item)
}, ojb)
数据结构转换
Map
转数组上文也提到,
Map
转数组只需要拓展运算符...
即可。jslet map = new Map([ [1, 'a'], [2, 'b'] ]) console.log([...map]) // [[1, 'a'], [2, 'b']]
数组转
Map
数组放到
new Map
的括号内就能转为Map
。jslet arr = [ [1, 'a'], [2, 'b'] ] const map = new Map(arr)
Map
转对象如果
Map
的每一个键都是字符串的形式,则可以通过代码逻辑转为Map
。jslet map = new Map([ ['yes', true], ['no', false] ]) const toObj = (params) => { let obj = Object.create(null) for (let [k, v] of params) { obj[k] = v } return obj } console.log(toObj(map)) // {yes: true, no: false}
对象转
Map
同理可得,可以通过代码逻辑把对象转为
Map
。jslet obj = { yes: true, no: false } const toMap = params => { let map = new Map() for (let k of Object.keys(params)) { map.set(k, params[k]) } return map } console.log(toMap(obj)) // Map(2) {'yes' => true, 'no' => false}
Map
转JSON
Map
转JSON
有两种情况:键都是字符串型:此时可以转为对象形式的
JSON
jsfunction toObjJSON(params) { return JSON.stringify(toObj(params)) // 使用上方`Map` 转对象中的方法 } let map = new Map([ ['a', 1], ['b', 2] ])
键不全是字符串型:此时转为数组形式的
JSON
JSON
转Map
JSON
转为Map
,正常情况下所有键名都是字符串jsfunction jsonToStrMap(jsonStr) { return objToStrMap (JSON .parse(jsonStr) ); } jsonToStrMap('{"yes": true, "no": false }') // Map {’ yes ’=> true,’no’ => false}
但是,有 种特殊情况:整个
JSON
就是 个数组,且每个数组成员本身又是 个具有两个成员的数组。这时,它可以一一对应地转为Map
。这往往是数组转为JSON
的逆操作。jsfunction jsonToMap(jsonStr) { return new Map(JSON.parse(jsonStr)) } jsonToMap('[[true, 7], [{"foo": 3}, ["abc"]]]') // Map {true=> 7 , Object {foo: 3) => [’ abc ’ ]}
WeakMap
含义
WeakMap
和 Map
相似,都是用 set()
生成键值对集合,get()
获取值,has()
判断是否存在,delete
删除键值对。
两者的区别在于:
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。如果用number
、string
等类型作为键名,会报错。WeakMap
的键名所指向的对象,不计入垃圾回收机制。垃圾回收机制是判断当前变量是否被使用到,如果被使用到就不会被回收,因此有时候如果使用了某些数据变量用完后没及时置空,则这些变量会一直占用内存,导致内存泄漏。
jslet obj1 = { a: 1, b: 2 } let arr = [obj1, 23]
而
WeakMap
的键名是弱引用,垃圾回收机制会自动回收键名所指向的对象,因此不会造成内存泄漏。变量被垃圾回收后,WeakMap
中对应的键值对也会自动消失,无法访问。
⚠ 注意
WeakMap
引用的只是键名而不是键值,键值依然是正常引用的。
语法
由于 WeakMap
是弱类型引用,因此某个键名是否存在完全不可预测,可能上一秒还存在,下一秒对象被垃圾回收机制运行回收了,消失找不到了。为了防止这种不确定性, ES6 规定, WeakMap
没有遍历操作,也没有 size
属性,也不支持 clear
方法。因此, WeakMap
只能用来存储键值对,而不能用来存放任何复杂的数据结构。
用途
WeakMap
的用途主要是用来存储私有数据,因为它的键名是弱引用,垃圾回收机制会自动回收键名所指向的对象,因此不会造成内存泄漏。
WeakMap
可以用来存储DOM
节点的私有属性,防止内存泄漏。jslet myElement = document.getElementById('logo') let myWeakmap = new WeakMap() myWeakmap.set(myElement, handler) myElement.addEventListener('click', myWeakmap.get(myElement), false)
WeakMap
可以用来部署私有属性。
const _counter= new WeakMap();
const _action= new WeakMap();
class Countdown {
constructor(counter, action) {
counter.set(this, counter);
action.set (this, action);
}
dec() {
let counter= _counter.get(this);
if (counter < 1) return ;
counter--;
_counter.set(this, counter) ;
if (counter === 0) {
_action.get(this) ();
}
}
}
const c = new Countdown(2, () => console.log('DONE '));
c.dec()
c.dee()
总结
ES6中新增了两种数据结构:Set
和 Map
,以及它们的弱引用版本 WeakSet
和 WeakMap
。Set
是一个元素唯一、无序且允许重复的数据结构,通过 new Set()
创建,支持 add
、delete
、has
等方法,并可通过 for...of
循环遍历。Map
是键值对的集合,键不仅限于字符串,支持 set
、get
、has
、delete
等方法,同样可通过 for...of
循环遍历。WeakSet
和 WeakMap
分别对应 Set
和 Map
的弱引用版本,只接受对象作为键,且键的引用为弱引用,有利于垃圾回收,但不可遍历且没有 size
属性。
Set
和 Map
的遍历操作包括 keys()
、values()
、entries()
和 forEach()
,保持插入顺序。Array.from()
可将 Set
转为数组,实现去重。Map
可以转换为数组,数组也可以转换为 Map
,两者互为逆操作。WeakMap
用于存储私有数据,防止内存泄漏,适用于存储 DOM
节点的私有属性和部署私有属性。