Set和Map数据结构

Set

Set是ES6提供的数据结构,类似于数组,但是成员的值是唯一的,木有重复。Set本身是一个构造函数

ps:构造函数可以创建多个对象的实例,开头字母要大写,this指向实例,使用new来生成实例对象

基本用法

1
const s = new Set()

add方法:向Set结构加入成员

1
2
[1,2,3,3].forEach(x=>s.add(x))
// 1 2 3

Set可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化。如:

1
2
3
const set = new Set([1,2,3,4,4])
[...set]
[1,2,3,4]

获取Set实例长度

1
2
const item = new Set([1,2,3,4,4])
item.size // 4

可以利用Set去除数组的重复成员,

1
[...new Set(array)]

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

1
2
3
4
function dedupe(array){
return Array.from(new Set(array))
}
dedupe([1,2,2,3,3]) // [1,2,3]

向Set加入值,不会使值发生类型转换,如5和”5”是两个不一样的值,Set内容判断是否相等的算法认为NaN等于本身,这点和===不一样。
另外,两个对象是不相等的

1
2
3
4
let set = new Set()
set.add({})
set.add({})
set.size // 2

Set实例属性和方法

  • add(val): 添加某个值, 返回Set结构本身
  • delete(val):删除某个值,返回一个布尔值,表示是否删除成功
  • has(val): 返回一个布尔值,表示参数是否为Set成员
  • clear():清除所有成员,没有返回值
    1
    2
    3
    4
    5
    let set = new Set()
    set.add(1).add(2)
    set.has(1) // true
    set.delete(2) // true
    set.has(2) // false

遍历操作

Set结构的实例有4种遍历方法:

  • keys(): 返回键名的遍历器
  • values(): 返回键值的遍历器
  • entries(): 返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员
    Set结构没有键名,只有键值(键名等于键值),所以keys和values方法行为是一样的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    let set = new Set(['green','red','yellow'])
    for(let item of set.keys()){
        console.log(item)
        // green
        // red
        // yellow
    }
    for(let item of set.values()){
        console.log(item)
        // green
        // red
        // yellow
    }
    for(let item of set.entries()){
        console.log("entries",item)
        // entries [ 'green', 'green' ]
        // entries [ 'red', 'red' ]
        // entries [ 'yellow', 'yellow' ]
    }
    Set实例默认可遍历,默认遍历器生成的函数就是values方法
    1
    2
    Set.prototype[Symbol.iterator] === Set.prototype.values
    // true
    这意味着可以使用for…of代替values遍历Set
    1
    2
    3
    4
    5
    6
    7
    let set = new Set(['red','green','yellow'])
    for(let x of set){
    console.log(x)
    }
    // red
    // green
    // yellow
    forEach方法用于对每个成员执行某些操作,没有返回值
    1
    2
    3
    4
    5
    6
    7
    let set = new Set([1,2,3])
    set.forEach((val,key)=>{
        console.log(val*2)
    })
    // 2
    // 4
    // 6
    forEach方法的参数是一个处理函数,该函数的参数依次是键值、键名、集合本身;另外,forEach方法还可以有第二个参数,表示绑定的this对象
  • 遍历的应用*
    数组的map和filter也可以用于Set
    1
    2
    3
    4
    let set = new Set([1,2,3])
    console.log([...set]) //  [1, 2, 3]
    set = new Set([...set].map(x=>x*2))
    console.log(set) // Set:{246}
    filter适合应用于求交集、并集、差集

    WeakSet

    WeakSet也是不重复值的集合,但是与Set有两个区别
  1. WeakSet的成员只能是对象
  2. WeakSet中的对象都是弱引用(即垃圾回收机制不管WeakSet还用不用这个对象,只要其他对象不引用这个对象,该对象就被回收了)
    WeakSet不存在内存泄漏的问题,所以适合临时存放一组对象
    ES6规定WeakSet不可遍历。

WeakSet有add,delete,has方法, 但是没有size属性和forEach方法

Map

JS的对象(Object)本质上是键值对的集合(hash结构),但是只能用字符作为键。
为了解决这个问题,ES6提供了Map数据结构。

含义

Map类似于对象,也是键值对的集合,但是键的类型可以是各种类型的值。Map比Object更适合“键值对”的数据结构。

简单应用

1
2
3
4
5
6
7
const m = new Map()
const o = {p:"hello,world"}
m.set(o,'content')
m.get(o) // content
m.has(o) // true
m.delete(o) // true
m.has(o) // false

Map也可以接受一个数组作为参数

1
2
3
4
5
6
const map = new Map([['name','John'],['title','Author']])
map.size // 2
map.has('name'// true
map.get('name'// John
map.has('title'// true
map.get('title'// Author

错误示范,外层数组漏加[]

1
2
3
const map = new Map(['name','John'],['title','Author'])
map.get('name'
// Iterator value name is not an entry object

除了数组,任何具有Iterator接口且每个成员都是一个双元素数组的数据结构都可以当作Map构造函数的参数。即Set和Map也可以作为Map构造函数的参数。

1
2
3
4
5
6
7
const set = new Set([['foo', 1], ['bar', 2]])
const m1 = new Map(set)
m1.get('foo') // 1

const m2 = new Map([['baz', 3]])
const m3 = new Map(m2)
m3.get('baz') // 3

如果对同一个键多次赋值,后面的值会覆盖前面的值

1
2
3
4
const map = new Map()
map.set(1, 'aaa')
map.set(1, 'bbb')
map.get(1) // 'bbb'

如果读取一个未知的键,则返回undefined

1
new Map().get('aaa') // undefined

注意,只有对同一个对象的引用,Map结构才将其视为同一个键

1
2
3
const map = new Map()
map.set(['a'], 555)
map.get(['a']) // undefined
1
2
3
4
const map = new Map()
let a = ['a']
map.set(a,666)
console.log(map.get(a)) // 666

同理,同样的值的两个实例在Map结构中也被视为两个键。

1
2
3
4
5
6
const map = new Map()
const k1 = ['a']
const k2 = ['a']
map.set(k1, 111).set(k2, 222)
map.get(k1) // 111
map.get(k2) // 222

由此可知,Map的键是和内存地址绑定的。
如果Map的键是一个简单数据类型的值(数字、字符串、布尔值),只要两个值严格相等(包括-0和+0)则为一个键。NaN虽然不严格相等,但是Map会把它视为一个键。

属性

size:返回Map结构的成员总数

方法

  • set(key, value): 设置key所对应的键值,然后返回整个Map结构。
    set方法返回的是当前的Map对象,所以可以采用链式写法。
    1
    2
    const map = new Map()
    map.set(1, 'a').set(2, 'b').set(3, 'c')
  • get(key): 读取key对应的键值,如果找不到key就会返回undefined
  • has(key): 返回一个布尔值,表示某个键是否在Map数据结构中存在。
  • delete:删除某个键,返回true,如果删除失败,则返回false。
  • clear(): 清除所有成员,没有返回值。

遍历方法

Map原生提供了3个遍历器生成函数和1个遍历方法。

  • keys(): 返回键名的遍历器
  • values(): 返回键值的遍历器
  • entries(): 返回所有成员的遍历器
  • forEach(): 遍历Map的所有成员。

需要注意的是,Map的遍历顺序就是插入排序,跟Set一样
Map结构默认的遍历器接口是entries方法。这意味着可以使用for…of代替entries方法。
Map结构转为数组结构的比较快速的方法就是结合扩展运算符(…)

1
2
3
4
5
const map = new Map([['aaa',111],['bbb',222],['ccc'3]]);
[...map.keys()]; // [ 'aaa', 'bbb', 'ccc' ]
[...map.values()]; // [ 111, 222, 3 ]
[...map.entries()]; // [ [ 'aaa', 111 ], [ 'bbb', 222 ], [ 'ccc', 3 ] ]
[map.values()] // [ [Map Iterator] { 111, 222, 3 } ]

结合数组的map方法、filter方法,可以实现Map的遍历和过滤(Map本身么有map和filter方法)。

1
2
3
4
5
6
7
8
9
10
11
const map = new Map([['aaa',111],['bbb',222],['ccc'3]]);
const map1 = new Map(
    [...map].filter(([k,v])=> v>3)
)
console.log(map1) 
// Map { 'aaa' => 111, 'bbb' => 222 }
const map2 = new Map(
    [...map].map(([k,v])=>[k+'hhh',v*2])
)
console.log(map2) 
// Map { 'aaahhh' => 222, 'bbbhhh' => 444, 'ccchhh' => 6 }

与其他数据结构的相互转换

Map转为数组
Map转为数组最方便的方法就是使用扩展运算符(…)。

1
2
3
4
5
const myMap = new Map()
.set(true,7)
.set({foo:0},['abc'])
console.log([...myMap]) 
// [ [ true, 7 ], [ { foo: 0 }, [ 'abc' ] ] ]

数组转为Map
将数组传入Map构造函数就可以转为Map。

1
2
3
4
5
const map = new Map([
    [true,0],
    [{foo:1},['abs']]
])
console.log(map) // Map { true => 0, { foo: 1 } => [ 'abs' ] }

Map转为对象
如果Map的所有键都是字符串,就可以转为对象。

1
2
3
4
5
6
7
8
9
10
function strMapToObj(strMap){
    let obj = Object.create(null)
    for(let [k,v] of strMap){
        obj[k] = v
    }
    return obj
}
const myMap = new Map().set('yes'true).set('no'false)
console.log(strMapToObj(myMap)) 
// [Object: null prototype] { yes: true, no: false }

对象转为Map

1
2
3
4
5
6
7
8
9
function strObjToMap(obj){
    let strMap = new Map()
    for(let k of Object.keys(obj)){
        strMap.set(k,obj[k])
    }
    return strMap
}
const myObj = {'yes':true,'no':false}
console.log(strObjToMap(myObj)) // Map { 'yes' => true, 'no' => false }

Map转为JSON
Map转为JSON有两种情况:

  • Map的键名都是字符串,这时可以转为JSON对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function strMapToObj(strMap){
        let obj = Object.create(null)
        for(let [k,v] of strMap){
            obj[k] = v
        }
        return obj
    }
    function strMapToJson(strMap){
        return JSON.stringify(strMapToObj(strMap))
    }
    const myMap = new Map().set('yes',true).set('no',false)
    console.log(strMapToJson(myMap)) 
    // {"yes":true,"no":false}
  • Map的键名非字符串,可以选择转为数组JSON
    1
    2
    3
    4
    5
    6
    function strMapToArrayJson(strMap){
        return JSON.stringify([...strMap])
    }
    const myMap = new Map().set(true,7).set({foo:3},['abc'])
    console.log(strMapToArrayJson(myMap)) 
    // [[true,7],[{"foo":3},["abc"]]]

JSON转为Map
也分为两种情况

  • 所有键名都是字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function strObjToMap(obj){
        let strMap = new Map()
        for(let k of Object.keys(obj)){
            strMap.set(k,obj[k])
        }
        return strMap
    }
    function jsonToStrMap(jsonStr){
        return strObjToMap(JSON.parse(jsonStr))
    }
    console.log(jsonToStrMap('{"yes":true,"no":false}'))
    // Map { 'yes' => true, 'no' => false }
  • JSON本身是一个数组,且数组成员本身又是具有两个成员的数组。它可以一一对应转为Map。
    1
    2
    3
    4
    5
    6
    function jsonToMap(jsonStr){
        return new Map(JSON.parse(jsonStr))
    }
    const myJson = '[[true,7],[{"foo":3},["abc"]]]'
    console.log(jsonToMap(myJson)) 
    // Map { true => 7, { foo: 3 } => [ 'abc' ] }
    注意JSON键名不要用’,不然报错SyntaxError: Unexpected token ‘ in JSON

WeakMap

WeakMap结构与Map结构类似,也用于生成键值对的集合。
但是跟Map有两点区别

  1. WeakMap只能接受对象作为键名(null除外)
  2. WeakMap的键名所指向的对象不计入垃圾回收机制。

基本上,如果要向对象中添加数据又不想干扰垃圾回收机制,就可以使用WeakMap,一个典型应用场景是,在网页的Dom元素上添加数据时就可以使用weakmap结构。
注意,WeakMap弱引用的只是键名而不是键值,键值依然是正常引用的
weakmap没有key()、values()、entries()方法,也没有size属性;只有set()、get()、delete()和has()方法。