ES6学习笔记之Set和Map

1,895 阅读8分钟

Set

基本用法

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set 本身是一个构造函数,用来生成 Set 数据结构。

const setArr = new Set();

[1,2,3,4,2,2,3,4].forEach(x => setArr.add(x));

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

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

  1. 以数组为参数, 可以去重
  2. Set 加入值的时候,不会发生类型转换,所以4和"4"是两个不同的值。
//  以数组为参数
const set = new Set([1, 2, 3, 4, 4, '4'])
[...set]  // [1, 2, 3, 4, '4']
set.size  // 5

// 一个类似数组的带 iterable 接口的对象
const set = new Set(document.querySelectorAll('div'))

Set结构转换成数组有两个简单的方法

[...set]  // [1, 2, 3, 4, '4']

Array.from(set)  // [1, 2, 3, 4, '4']

Set 实例的属性和方法

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

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

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。

四个操作方法。

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

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2)
s.size  // 1
s.has(2) // false

四个遍历方法

  1. keys():返回键名的遍历器
  2. values():返回键值的遍历器
  3. entries():返回键值对的遍历器
  4. forEach():使用回调函数遍历每个成员

keys方法、values方法、entries方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

const set = new Set(['a', 'b', 'c']);

for (let item of set.keys()) {
  console.log(item);
}
// a
// b
// c

for (let item of set.values()) {
  console.log(item);
}
// a
// b
// c

for (let item of set.entries()) {
  console.log(item);
}
// ["a", "a"]
// ["b", "b"]
// ["c", "c"]

对象结构 在使用这个几个方法的时候类似,但有一定区别

const obj = { 1: 'a', 2: 'b', 3: 'c' }
for (let item of Object.keys(obj)) {
  console.log(item);
}
// 1
// 2
// 3

for (let item of Object.values(obj)) {
  console.log(item);
}
// a
// b
// c

for (let item of Object.entries(obj)) {
  console.log(item);
}
// [1, "a"]
// [2, "b"]
// [3, "c"]

Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。
这意味着,可以省略values方法,直接用for...of循环遍历 Set

for (let i of set) {
  console.log(i)
}
// a
// b
// c

forEach(): Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。
不过keyvalue是同一个

set.forEach((value, key) => console.log(key + ' : ' + value))
// a: a
// b: b
// c: c

遍历的应用

  1. 去重
  2. 实现并集(Union)、交集(Intersect)和差集(Difference)

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...union].filter(x => !intersect.has(x)));
// Set {1}

如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。

// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6

WeakSet

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

(1):WeakSet 可以接受数组和类似数组的对象作为参数。该数组的所有成员都会自动成为WeakSet的实例对象的成员。数组成员只能是对象,不能是其他类型的值。否则报错。

const a = [[1, 2], [3, 4], {a: 1}]
const ws = new WeakSet(a)
// WeakSet {[1, 2], [3, 4]}

const b = [1, 2, [1,2]]
new WeakSet(b)  // Uncaught TypeError: Invalid value used in weak set

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

Map

含义和基本用法

一个常规的对象本质应该是键值对的合集(即Hash结构)。它的键应该是一个字符串。 但是有时候需要使用其他类型比如对象来做 键值对的 键。 于是就有了Map结构

const data = {};
const element = document.getElementsByTagName('div')

data[element] = 'div';
data[element]  //  "div"
data['[object HTMLCollection]'] // "div"

// {[object HTMLCollection]: "div"}

const elementSpan = document.getElementsByTagName('span')
data[elementSpan] //  "div"

element 被转化成了'[object HTMLCollection]' 只是个字符串。 并不能达到通过element 取到值的效果。但是,使用Map结构可以。 Map结构与对象很相似也是键值对, 但Map 的键可以不是字符串,可以是各种类型的值(包括对象)。如果你需要“键值对”的数据结构,MapObject 更合适。

const dataMap = new Map()
dataMap.set(element, 'div')
dataMap.get(element)  // div

//  has,delete
dataMap.has(element)    //  true
dataMap.delete(element) //  true
dataMap.has(element)    //  false

以上 new Map() 实例对象为一个Map结构,提供了setgethasdelete几个方法。轻松了实现了增删改查。 构造函数Map 可以接受一个数组作为参数。

const map = new Map([ ['key1', 'value1'], ['key2', 'value2'] ])

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。如:数组,Set结构, Map结构。

const set = new Set([['a', 1],['b', 2]]);
const m1 = new Map(set);
m1.get('a') // 1

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

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

const k1 = {a: 1 }
const k2 = {a: 1 }
const map = new Map()

map.set(k1, 111)
map.get(k2) // undefined

map.set(k2, 222)
map.get(k1) //  111
map.get(k2) //  222

因为即便值相同,但是两个对象内存地址是不一样的。

这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键。

实例的属性和操作方法

  1. set(key, value): set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
  2. get(key):get方法读取key对应的键值,如果找不到key,返回undefined
  3. size 属性: 返回 Map 结构的成员总数。
  4. has(key): has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  5. delete(key): delete方法删除某个键,返回true。如果删除失败,返回false
  6. clear(): clear方法清除所有成员,没有返回值。
const map = new Map();
//  可以采用链式写法。
map.set('a', 1).set('b', 2)

map.size          // 2

map.get('b')      // 2
map.set('b', 222)
map.get('b')      // 222
map.get('c')      // undefined

map.has('b')      // true

map.delete('b')   // true
map.has('b')      // false

map.clear()
map.size          // 0

遍历方法

  1. keys():返回键名的遍历器
  2. values():返回键值的遍历器
  3. entries():返回键值对的遍历器
  4. forEach():使用回调函数遍历每个成员

需要特别注意的是,Map 的遍历顺序就是插入顺序。

const map = new Map([['a', 1], ['b',  2]]);

for (let key of map.keys()) {
  console.log(key);
}
// "a"
// "b"

for (let value of map.values()) {
  console.log(value);
}
// 1
// 2

for (let item of map.entries()) {
  console.log(item);
}
// ["a", 1]
// ["b", 2]

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "a" 1
// "b" 2

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "a" 1
// "b" 2

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

(1)Map 与数组的互换

const arr1 = [[{'k1': 11}, 11 ],['a', 1]]
const map = new Map(arr1)

[...map]          // [[{'k1': 11}, 11 ],['a', 1]]
Array.from(map)   // [[{'k1': 11}, 11 ],['a', 1]]

(2) Map 与对象的互换

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

const obj1 = { a:1, b:2 }
const obj2 = {}
const map = new Map()

for(let key of Object.keys(obj1)) {
  map.set(key, obj1[key])
}

console.log(map)    // Map(2) {"a" => 1, "b" => 2}

for (let [key,value] of map) {
  obj2[key] = value
}

console.log(obj2)  // {a: 1, b: 2}

(3) JSON 要转换成 Map 可以先转换成数组或者对象,然后再转换。

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。

const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

WeakMap与Map的区别有两点。

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  2. WeakMap的键名所指向的对象,不计入垃圾回收机制。
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!

WeakMap 的语法:

WeakMapMap 相似但有两个区别:

  1. 没有遍历操作(即没有key()、values()和entries()方法)。
  2. 无法清空,即不支持clear方法。 因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。

WeakMap 的用途

WeakMap 应用的典型场合就是 DOM 节点作为键名。 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.dec()

习题: 一· 求set 的值

let arr1 = [1, 2, 3, '3', 2]
const set = new Set(arr1)

二. 求遍历的输出

for (let [key, value] of set.entries()) {
  console.log(key === value)
}

三. set 转换数成数组
四. 求 data[obj1],

const obj1 = { a: 1}
const obj2 = { b: 2}

const data = {}
data[obj1] = 11
data[obj2] = 22

// 求 data[obj1]

五. 1.求 map.get({a: 1}), 2.如果 map 要转换成其他结果,应该是对象还是数组

const map = new Map()
map.set({a: 1}, 111)

map.get({a: 1})