ES6入门之Set 和 Map

1. Set

1.1 基本用法

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

const s = new Set();

[2,3,4,5,6,4,2,2,7].forEach(x => s.add(x))

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

// Set 不会添加重复的值。

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

// 实例一
const set = new Set([1,2,3,4,5,5])
[...set] // [1,2,3,4,5]

// 实例二
const items = new Set([1,2,3,3,4])
items.size // 4

// 上面代码还展示了数组去重的方法

[...new Set(array)]

// 或者字符串去重

[...new Set('aabbcc')].join('')

向 Set 加入值的时候,不会发生类型转换, 所有**5 和 ‘5’**两个不同的值。在Set 内部判断两个值是否不同,使用的算法叫做 ‘Same-value-zero equality’,它和 ‘===’类似,区别在于 向 Set 加入值的时候认为 NaN等于自身,而 ‘===’ 则认为 NaN 不等于自身

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
// 如上,只能向Set中添加一个NaN则说明了刚刚的证明

在 Set中两个对象总是不相等的。

let set = new Set();
set.add({})
set.size // 1

set.add({})
set.size // 2

1.2 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() // 清除所有成员,没用返回值

s.add(1).add(2).add(2);
// 注意2被加入了两次

s.size // 2

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

s.delete(2);
s.has(2) // false

// Object 和 Set 如何判断一个键的区别

// 对象
const p = {
    'w': 1,
    'h': 2
}
if(p[a]){
    no
}

// Set
const p = new Set()
p.add('w')
p.add('h')
if(p.has(c)){
    no
}

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

const items = new Set([1,2,3,4,5])
const array = Array.from(items)

// 数组去重的另外一种方法

function d(a){
    return Array.from(new Set(a))
}
d([1,2,3,3,5])

1.3 遍历操作

Set 结构的实例有四个遍历方法,用于遍历成员

  • Set.prototype.keys() // 返回键名的遍历器
  • Set.prototype.values() // 返回键值的遍历器
  • Set.prototype.entries() // 返回键值对的遍历器
  • Set.prototype.forEach() // 使用回调函数遍历每个成员

注意:Set遍历顺序就是插入顺序,这个特性在特定情况非常有用,比如使用Set保存一个回调函数列表,调用时就能保证按照添加顺序调用。

1.3.1 keys()、values、entries()

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

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
  console.log(item);
}
// red
// green
// blue

for (let item of set.entries()) {
  console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]

注意:Set结构的实例默认可遍历,它的默认遍历器生成函数就是它的 values方法,这样我们就可以省略 values,直接用 for…of循环遍历Set

1.3.2 forEach()

Set结构的实例和数组一样,也有 forEach方法,用于对每个成员执行某种操作,没用返回值。forEach 可以用第二个参数表示绑定处理函数内部的 this 对象。

1.3.3 遍历的应用

扩展运算符(…)内部使用 for…of循环样能用于 Set 结构

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]用来去重操作

// 实现 并集、交集、差集
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([...a].filter(x => !b.has(x)));
// Set {1}

如果想在遍历操作中同步改变原理的Set结构,只能利用原有的Set结构映射一个新的结构,然后赋值给原来的Set,另一个就是通过 Array.from方法。

// 方法一
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

2. WeakSet

WeakSet 结构与Set类似,也不是重复的值的集合,但是和Set有两个区别,第一个它的成员只能为对象,另一个它的对象都是弱印象,即垃圾回收机制不考虑 WeakSet对该对象的引用,通俗的讲就是,如果该对象没用在其他对象中被引用,那么该对象就会被回收,不会考虑这个对象是否在 WeakSet中。

依赖于垃圾回收进制依赖引用计数,如果一个值的引用次数不为0,那么就不会被回收,但是有的时候,结束使用该值后,会忘记取消引用,就会导致内存无法释放从而导致内存泄漏。但是 WeakSet里面的而引用不会计入垃圾回收机制,所以适合存放临时的对象,一旦外部消失,那么WeakSet里面的引用就会自动消失。

基于以上的特点,WeakSet 成员不适合被引用,所以 WeakSet无法被遍历。

2.1 语法

它也是一个构造函数,可以通过 new 来创建

const ws = new WeakSet()

// 做为构造函数,WeakSet

可以接受一个数组或类似数组的对象作为参数,该数组的所有成员,
都会自动成为 WeakSet实例对象的成员。

注意:只能是数组的成员成为WeakSet的成员,而不是 a 数组本身,这就意味着,数组的成员只能是对象。

2.2 WeakSet的方法

  1. WeakSet.prototype.add(value): 向 WeakSet实例添加一个新成员。
  2. WeakSet.prototype.delete(value): 清除 WeakSet实例的指定成员。
  3. WeakSet.prototype.has(value): 返回一个布尔值,表示某个值是否存在 WeakSet实例中。

注意: WeakSet 同样没有size 属性,不能遍历其成员。

3. Map

JavaScript的对象,本质上是键值对的集合,但是传统上只能字符串当做键,这给他带来了很大的限制。Map的出现,就是让各种类型的值都可以当作键。Map提供的是 “值-值”的对应。

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

注意:Set 和 Map 都可以用来生成新的Map,如果对同一个键多次赋值,那么前面的将被后面的值覆盖。另外只有对同一个对象的引用,Map结构才将其视为同一个键。另外同样的两个实例,在Map中将被视为两个键。

总结:综上所述,Map的键实际上跟内存地址绑定的,只要内存地址不一样,就视为两个键。这样就可以解决同名属性碰撞的问题。如果我们扩展别人库的时候,如果使用对象最为键名,就不用担心自己的属性与原作者属性冲突。

如果Map的键是一个简单类型的数值,则只要两个值严格相等,Map将其视为一个键,0 和 -0 是一个键,true 和 ‘true’则是两个不同的键, undefined 和 null 也是两个不同的键, 另外 NaN 在Map 中视为同一个键

3.1 Map的属性和操作方法

1. size 属性

size 属性返回Map结构的成员总数

const map = new Map()
map.set('foo', ture)
map.set('bar', false)

map.size // 2
2. Map.prototype.set(key, value)

set 方法设置键名 key 对应的键值为 value,然后返回整个 Map 结构。如果 key 已经有值,则键值会被更新,否则就新生成该键。

const m = new Map()
m.set('e', 6)   // 键值是字符串
m.set(2, 's')   // 键是数值
m.set(undefined, 'n')   // 键是 undefined

// set方法返回的是当前的Map对象,因此可以采用链式写法

let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c')
3. Map.prototype.get(key)

get 方法读取 key 对应的键值,如果找不到key就返回 undefined

const m = new Map();

m.set('c',  124)
m.get('c')  // 124
4. Map.prototype.has(key)

返回一个布尔值,用来表示某个键是否在当前 Map 对象中

let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c')

map.has(1)  // true
map.has(4)  // false
5. Map.prototype.delete(key)

delete方法删除某个键,返回 true,如果删除失败,则返回 false

let map = new Map()
    .set(1, 'a')
    .set(2, 'b')
    .set(3, 'c')

map.delete(1)   // true
map.delete(4)   // false
6. Map.prototype.clear()

clear 方法清除所有成员,没有返回值

let map = new Map()
    .set(1, 'a')
    .set(2, 'b')
    .set(3, 'c')

map.size    // 3
map.clear() 
map.size    // 0

3.2 遍历方法

  1. Map.prototype.keys(): 返回键名的遍历器
  2. Map.prototype.values(): 返回键值的遍历器
  3. Map.prototype.entries(): 返回所有成员的遍历器
  4. Map.prototype.forEach(): 遍历Map的所有成员

注意:Map的遍历顺序就是插入顺序。

Map 结构转为数组结构,比较快速的方法是使用扩展运算符(…),另外Map可以通过 forEach 可以实现遍历。

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()]
// [1, 2, 3]

[...map.values()]
// ['one', 'two', 'three']

[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

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

1. Map转为数组

通过扩展运算符(…)

2. 数组转为Map

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

new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
3. Map 转为对象

如果Map的键都是字符串,它可以无损地转为对象,如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

4. 对象转为 Map
5. Map 转为 JSON

Map转为JSON要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象JSON。另外一种情况是,Map 的键名有非字符串,这时可以选择转为数组JSON

6. JSON 转为 Map

JSON转为Map,正常情况下,所有键名都是字符串。但是,有一种特殊情况,整个JSON就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为Map.

4. WeakMap

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

WeakMap 和 Map 的区别

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

如果 我们想在某个对象上面存放以未数据,但是会形成对于这个对象的引用,如果我们不需要这两个对象,就必须手动删除,否则垃圾回收机制就不会释放占用的内存。

WeakMap 就是为了解决这个问题而诞生,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用被清除,垃圾回收机制就会释放该对象所占用的内存,也就是说一旦不再需要,WeakMap里面的键名对象和所对应的键值对会自动消失,不用手动删除。

注意: WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用。

4.1 WeakMap 的语法

WeakMap 与 Map 在 API的区别主要有两个,一是没有遍历操作(没有keys,values,entries),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一个垃圾回收机制突然运行了,这个键名就没有了。为了防止不确定性,就统一规定不能取到键名。二是无法情况,即不支持 clear 方法。因此 WeakMap 有四个方法: get()、set()、has()、delete()

const wm = new WeakMap();

// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined

4.2 WeakMap的用途

  1. 应用于 DOM 节点作为键名
  2. WeakMap 的另一个用处是部署私有属性
  3. 就是防止内存泄漏的风险

欢迎关注 公众号【小夭同学】