ES6 Set和Map数据结构

Map结构以及Set结构的用法整理。

Set

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

1
2
3
4
5
6
const s = new Set();
[1, 2, 3, 3, 2, 1].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 输出:1, 2, 3

运行上面的例子,通过一个数组数据源向Set结构加入成员,结果表明Set结构不会添加重复的值。

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

1
2
3
const s = new Set([1, 2, 3, 4, 3, 2, 1]);
console.log(...s);
// 输出:1, 2, 3, 4

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

1
2
3
4
5
6
const s = new Set([NaN, NaN]);
console.log(...s); // 输出:NaN
console.log(NaN === NaN);
console.log(Number('a')); // 输出:NaN
console.log(isNaN(Number('a'))); // 输出:true
console.log(Number('a') === NaN); // 输出:false

上面例子可以看到:

  1. 向Set添加两个NaN,只能加入一个,表明在Set内部两个NaN是相等的。
  2. 直接使用精确相等运算符判断两个NaN是不相等的。
  3. 要判断一个值是否是NaN只能使用方法isNaN(x)来判断。

需要注意的是,两个对象总是不相等的。例如:

1
2
const s = new Set([{}, {}]);
console.log(s.size); // 输出:2

上面表明,两个空对象都被加入到了Set结构中。

Set实例的属性

  • Set.prototype.constructor 构造函数,创建Set数据结构。
  • Set.prototype.size 返回Set实例的成员总数

Set实例方法

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

四个操作方法

  • add(value) 添加某个值,返回Set结构本身。
  • delete(value) 删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value) 返回一个布尔值,表示该值是否为Set的成员。
  • clear() 清除所有的成员,没有返回值。

使用实例:

1
2
3
4
5
6
7
8
9
const s = new Set();
s.add(1).add(2).add({}).add(1);
console.log(s.size); // 输出:3
console.log(s.has({})); // 输出: false,对象总是不相等的。
console.log(s.has(2)); // 输出:true
console.log(s.delete(2)); // 输出: true
console.log(s); // 输出:1, {}
s.clear();
console.log(s); // 输出 Set{}

Arrary.form方法可以将Set结构转换为数组:

1
2
3
const s = new Set([1, 2, 4, 3, 2, 1]);
const arr = Array.from(s);
console.log(arr); // 输出:1,2,4,3

利用这一特性可以给数组去重:

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

四个遍历方法

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

特别说明的是,Set的遍历顺序就是插入顺序。这个特性非常有用,比如使用Set保存一个回调函数列表,调用时就能保证按照添加顺序调用。

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

1
2
3
4
const s = new Set([1, 2, 3]);
console.log(...s.keys()); // 输出:1 2 3
console.log(...s.values()); // 输出:1 2 3
console.log(...s.entries()); // 输出:[ 1, 1 ] [ 2, 2 ] [ 3, 3 ]

entries方法返回的遍历器同时包括键名和键值,所以每次输出一个数组,键名和键值完全相等。

Set结构可以默认遍历,它的默认遍历器生成的函数就是它的values()方法:

1
2
console.log(Set.prototype[Symbol.iterator]); // [Function: values]
console.log(Set.prototype[Symbol.iterator] === Set.prototype.values); // true

所以我们可以直接使用for...of遍历Set:

1
2
3
4
const s = new Set([1, 2, 3]);
for(let i of s){
console.log(i);
}

forEach()方法用于对每个成员执行某种操作,没有返回值。该函数的参数与数组的forEach一致,依次为键值、键名、集合本身。需要注意的是:Set结构的键值和键名是同一个值。因此第一个参数和第二个参数的值永远都是一样的。

另外,forEach()还可以有第二个参数,表示绑定处理函数内部的this对象。例如:

1
2
3
4
5
6
const s = new Set([1, 2, 3]);
const obj = [];
s.forEach(function(value) {
this.push(value);
}, obj);
console.log(obj); // 输出:1, 2, 3

遍历的应用

扩展运算符...内部使用for...of循环,所以也可以用于Set结构,扩展运算符和Set结构结合使用,可以简洁的给数组去重:

1
2
3
const arr = [1, 2, 3, 1, 2];
const unique = [...new Set(arr)];
console.log(unique); // 输出:[ 1, 2, 3 ]

数组的mapfilter也可以间接用于Set,可以很容易的实现并集(union)、交集(intersect)和差集(difference):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arr1 = [1, 2, 3, 1, 2];
const arr2 = [3, 4, 3, 2, 4];

const set1 = new Set(arr1);
const set2 = new Set(arr2);
// 并集
const union = new Set([...set1, ...set2]);
console.log(union); // Set { 1, 2, 3, 4 }
// 交集
const intersect = new Set([...set1].filter(x => set2.has(x)));
console.log(intersect); // Set { 2, 3 }
// 差集
const difference = new Set([...set1].filter(x => !set2.has(x)));
console.log(difference); // Set { 1 }

Map

JavaScript的对象(Object)本质上是键值对的集合(Hash结构)。但是传统上只能使用字符串当做键,这使得使用带来很大的限制。为了解决这个问题,ES6提供了Map数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当做键。也就是说,Object结构提供了“字符串-值”的对应,Map结构提供了“值-值”的对应。是一种更完善的Hash结构实现。如果你需要“键值对”数据结构,Map比Object更合适。

Map是一个构造函数,可以直接构造Map实例:

1
2
3
4
5
6
7
const m = new Map([
['name', 'tony'],
['age', 18],
]);
console.log(m.size); // 2
console.log(m.get('name')); // tony
console.log(m.has('age')); // true

Map构造函数在接受数组作为参数时,实际上是执行下面的过程:

1
2
3
4
5
6
7
8
const items = [
['name', 'tony'],
['age', 18],
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);

不仅仅是数组,任何具有Iterator接口、且每个成员都是一个双元素的数组的数据结构都可以当做Map构造函数的参数。也就是说,SetMap都可以用来生成新的Map。

1
2
3
4
5
6
const set = new Set([
['name', 'tony'],
['age', 18],
]);
const map = new Map(set);
console.log(map.get('name')); // tony

Map 实例的属性

和Set结构一样,有两个属性:

  • 构造方法,生成一个Map实例
  • size属性,返回Map结构的成员总数
1
2
3
4
5
const map = const set = new Set([
['name', 'tony'],
['age', 18],
]);
console.log(map.size); // 2

Map 实例的方法

操作方法

  • set(key, value) 给Map设置键名key对象的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就生成该键值对。
  • get(key) 读取key对应的键值,如果找不到key,返回undefined
  • has(key) 返回一个布尔值,表示某个键是否在当前的Map对象中。
  • delete(key) 删除某个键,删除成功为true,删除失败为false。
  • clear() 清除所有成员,没有返回值。

对一个键多次赋值,后面的值覆盖前面的值:

1
2
3
const map = new Map();
map.set(1, 'a').set(1, 'b');
console.log(map.get(1)); // b

读取一个未知的键返回undefined

1
2
const map = new Map();
console.log(map.get(1)); // undefined

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

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

上面的setget看起来是同一个键,但实际上是两个值,内存地址不一样,因此get无法获取该键,返回undefined。可以推理,同样的值的两个实例,在Map结构中被视为两个键:

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

从上面的例子可以得出,Map的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题。我们在扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map就将其视为一个键,比如0-0就是一个键,布尔值true和字符串'true'是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但Map将其视为同一个键(这点和Set结构一致)。

1
2
3
4
5
6
7
8
const map = new Map();
map.set(0, 123).set(-0, 'a');
console.log(map.get(+0)); // 123
map.set(undefined, 3).set(null, 4);
console.log(map.get(null)); // 4
console.log(map.get('')); // undefined
map.set(NaN, 5).set(Number('dd'), '33');
console.log(map.get(NaN)); // 33

遍历方法

Map结构提供三个遍历器生成函数和一个遍历方法:

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

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

1
2
3
4
5
6
7
const map = new Map();
map.set({name: 'tony'}, 3);
map.set({name: 'tony'}, 4);
map.set({name: 'tony'}, 5);
console.log(...map.keys()); // { name: 'tony' } { name: 'tony' } { name: 'tony' }
console.log(...map.values()); // 3 4 5
console.log(...map.entries()); // [ { name: 'tony' }, 3 ] [ { name: 'tony' }, 4 ] [ { name: 'tony' }, 5 ]

可以用for…of来遍历Map:

1
2
3
4
5
6
7
for (let [key, value] of map) {
console.log(key, value);
}
// 输出:
// { name: 'tony' } 3
// { name: 'tony' } 4
// { name: 'tony' } 5

可以发现,Map的默认遍历器接口是entries方法:

1
2
console.log(Map.prototype[Symbol.iterator]);
// 输出:[Function: entries]

Map的forEach方法与数组、Set的forEach方法类似,可以实现遍历,也可以接受第二个参数,用来绑定this。例如:

1
2
3
4
5
6
7
8
9
10
 const map = new Map();
map.set(1, 'a').set({name: 'tony'}, 'b').set(true, false);

map.forEach((value, key, map) => {
console.log('key: %s, value: %s', JSON.stringify(key), value);
});
// 输出:
// key: 1, value: a
// key: {"name":"tony"}, value: b
// key: true, value: false

没啥想总结的,技术整理~🙄

码字辛苦,打赏个咖啡☕️可好?💘