Map and Set(映射和集合)

Map映射

基本定义

JS的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。

const m = new Map();
const o = { p: 'jiuyi' }
console.log('o', o)
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false

Map的算法

const items = [
  ['name', '张三'],
  ['title', 'Author']
];

const map = new Map();

items.forEach(
  ([key, value]) => map.set(key, value)
);

Map和Set都可以生成新的map

const map = new Map([
    ['name', '张三'],
    ['title', 'Author']
]);
const set = new Set([
    ['foo', 1],
    ['bar', 2]
]);
console.log('map', map)
console.log('set', set)
// map Map(2) { 'name' => '张三', 'title' => 'Author' }
// set Set(2) { [ 'foo', 1 ], [ 'bar', 2 ] }
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
console.log('m2', m2)
console.log('m3', m3)
// m2 Map(1) { 'baz' => 3 }
// m3 Map(1) { 'baz' => 3 }

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

  • 注意,只有对同一个对象的引用,Map 结构才将其视为同一个键
  • 同样的值的两个实例,在 Map 结构中被视为两个键
    • Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。
  • Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键
const map = new Map([
    ['name', '张三'],
    ['title', 'Author']
]);
map.set('name', '极右翼')
console.log('map', map)
// map Map(2) { 'name' => '极右翼', 'title' => 'Author' }

map.set(['a'], 555);
console.log(map.get(['a']))// undefined
// set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,
//内存地址是不一样的,因此get方法无法读取该键,返回undefined。

const k1 = ['a'];
const k2 = ['a'];
// 变量k1和k2的值是一样的,但是它们在 Map 结构中被视为两个键。
map
    .set(k1, 111)
    .set(k2, 222);
console.log('map', map)
// map Map(5) {    'name' => '极右翼',    'title' => 'Author',
//     [ 'a' ] => 555,
//     [ 'a' ] => 111,
//     [ 'a' ] => 222
//   }

map[key] 不是使用 Map 的正确方式

虽然 map[key] 也有效,例如我们可以设置 map[key] = 2,这样会将 map 视为 JavaScript 的 plain object,因此它暗含了所有相应的限制(仅支持 string/symbol 键等)。

Map的实例属性和操作方法

const map = new Map();
const m4 = function () {
    console.log("hello", "hello")
}
map.set('m1', 6).set(6, 'm2')
map.set(undefined, 'm3')
map.set(m4, 'Hello ES6')
console.log('map', map) //map Map(3) { 'm1' => 6, 6 => 'm2', undefined => 'm3' , [Function: m4] => 'Hello ES6'}
console.log('map.size', map.size) //4
console.log('map.get', map.get('m1')) //6
console.log('map.get', map.get('m2')) //undefined
console.log('map.has', map.has('m1')) //true
console.log('map.has', map.has('m5')) //false
console.log('map.delete', map.delete(6)) //true
console.log('map.delete', map.delete('m5')) //false
console.log('map', map) //{ 'm1' => 6, undefined => 'm3', [Function: m4] => 'Hello ES6' }
map.clear();
console.log('map.size', map.size) //0

Map.prototype.set(key, value)

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

Map 还可以使用对象作为键

let john = { name: "John" };
// 存储每个用户的来访次数
let visitsCountMap = new Map();
// john 是 Map 中的键
visitsCountMap.set(john, 123);
console.log( visitsCountMap.get(john) ); // 123

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

size

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

Map.prototype.get(key)

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

Map.prototype.has(key)

has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

Map.prototype.delete(key)

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

Map.prototype.clear()

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

Map遍历方法

Map 的遍历顺序就是插入顺序

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。
const map = new Map([["A", 11], ['B', 22]]);
console.log('map', map)
const mapKeys = map.keys();//{ 'A', 'B' }
for (var key of mapKeys) {
    //key A
    //key B
    console.log('key', key)
}
const mapValues = map.values();
for (let value of mapValues) {
    console.log('value', value)
    // value 11
    // value 22
}
const mapEntries = map.entries();//{ [ 'A', 11 ], [ 'B', 22 ] }
for (let entry of mapEntries) {
    console.log('entry', entry)
    // entry [ 'A', 11 ]
    // entry [ 'B', 22 ]
}
for (let [key, value] of map.entries()) {
    console.log(key, value);
    // A 11
    // B 22
}
// 等同于使用map.entries()
for (let [key, value] of map) {
    console.log(key, value);
}
//表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法.
console.log('map[Symbol.iterator] === map.entries', map[Symbol.iterator] === map.entries) //true

map.forEach((value, key, map) => {
    console.log('value', value) //11 22
    console.log('key', key) //A B
    console.log('map', map) //map Map(2) { 'A' => 11, 'B' => 22 }

})
//forEach方法还可以接受第二个参数,用来绑定this。
const reporter = {
    report: function (key, value) {
        // Key: A, Value: 11,Map: Map(2) { 'A' => 11, 'B' => 22 }
        // Key: B, Value: 22,Map: Map(2) { 'A' => 11, 'B' => 22 }
        console.log("Key: %s, Value: %s,Map: %s", key, value, map);
    }
};
map.forEach(function (value, key, map) {
    this.report(key, value, map);
}, reporter);

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

Map 转为数组

使用扩展运算符(...)

const myMap = new Map()
    .set(true, 7)
    .set({ foo: 3 }, ['abc']);
console.log('myMap', myMap) // { true => 7, { foo: 3 } => [ 'abc' ] }
const mapArr = [...myMap]
console.log('mapArr', mapArr) //[ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

const myMap1 = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three'],
]);
console.log(myMap1);//{ 1 => 'one', 2 => 'two', 3 => 'three' }
console.log([...myMap1]);//[ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ] ]
console.log([...myMap1.keys()]);//[ 1, 2, 3 ]
console.log([...myMap1.values()]);//[ 'one', 'two', 'three' ]
console.log([...myMap1.entries()]);//[ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ] ]

数组转为 Map

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

const map = new Map([['a', 1], ['b', 2]]);
console.log('map', map) //{ 'a' => 1, 'b' => 2 }

Map转对象

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

const mapStrToObj = (strMap) => {
  let obj = Object.create(null)
  for (let [value, key] of strMap) {
    obj[key] = value
  }
  return obj
}
const obj1 = mapStrToObj(map)
console.log('obj1', obj1) //{ '1': 'a', '2': 'b' }
  • Object.fromEntries 方法的作用是相反的:给定一个具有 [key, value] 键值对的数组,它会根据给定数组创建一个对象
const obj = Object.fromEntries([
    ['banana', 1],
    ['orange', 2],
    ['meat', 4]
]);
console.log(obj);//{ banana: 1, orange: 2, meat: 4 }

对象转为 Map

对象转为 Map 可以通过Object.entries()。

let obj = { "a": 1, "b": 2 };
let map1 = new Map(Object.entries(obj))
console.log('map', map1) //Map(2) { 'a' => 1, 'b' => 2 }

function objToMapStr(obj) {
    let strMap = new Map();
    for (let key of Object.keys(obj)) {
        strMap.set(key, obj[key]);
    }
    return strMap;
}

const map = objToMapStr({ yes: true, no: false })
console.log('map', map)// { 'yes' => true, 'no' => false }

Map 转为 JSON

Map 转为 JSON 要区分两种情况。

  • Map 的键名都是字符串,可以选择转为对象 JSON。
  • Map 的键名有非字符串,可以选择转为数组 JSON。
// 对象 JSON
const mapStrToObj = (strMap) => {
    let obj = Object.create(null)
    for (let [value, key] of strMap) {
        obj[key] = value
    }
    return obj
}
let myMap = new Map().set('yes', true).set('no', false);
const jsonData = JSON.stringify(mapStrToObj(myMap))
console.log('jsonData', jsonData)

//数组json
let myMap1 = new Map().set(true, 7).set({ foo: 3 }, ['abc']);
console.log('myMap1', myMap1) //Map(2) { true => 7, { foo: 3 } => [ 'abc' ] }

const arrJson = JSON.stringify([...myMap1])
console.log('arrJson', arrJson) //[[true,7],[{"foo":3},["abc"]]]

JSON 转为 Map

  • JSON 转为 Map,正常情况下,所有键名都是字符串。
  • 有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
// 所有键名都是字符串
function objToMapStr(obj) {
    let strMap = new Map();
    for (let key of Object.keys(obj)) {
        strMap.set(key, obj[key]);
    }
    return strMap;
}

const map = objToMapStr(JSON.parse('{"yes": true, "no": false}'))
console.log('map', map) //Map(2) { 'yes' => true, 'no' => false }

//另一种情况
const map1 = new Map(JSON.parse('[[true,7],[{"foo":3},["abc"]]]'))
console.log('map1', map1) //Map(2) { true => 7, { foo: 3 } => [ 'abc' ] }

\

yin

Set集合

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

Array和Set对比

都是一个存储多值的容器,两者可以互相转换,但是在使用场景上有区别。如下:

  • Array的indexOf方法比Set的has方法效率低下
  • Set不含有重复值(可以利用这个特性实现对一个数组的去重)
  • Set通过delete方法删除某个值,而Array只能通过splice。两者的使用方便程度前者更优
  • Array的很多新方法map、filter、some、every等是Set没有的(但是通过两者可以互相转换来使用)

Set结构的属性

Set.prototype.constructor //构造函数,默认就是Set函数。
Set.prototype.size //返回Set实例的成员总数。
// 例二
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
console.log(set.size); // 5

Set 实例的操作方法

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

可以使用数组来初始化一个 Set ,并且 Set 构造器会确保不重复地使用这些值:

let set = new Set();
set.add(2);
set.add("2");
console.log(set.size); // 2
console.log(set.has(1)); // true
console.log(set.has(6)); // false
set.delete(1);
console.log(set.size); // 1
set.clear();
console.log(set.size); // 0

// 例三看看在判断是否包括一个键上面,Object结构和Set结构的写法不同
let spjObj = {
    'a': 1,
    'b': 2
}
if (spjObj.a) {
    console.log(spjObj.a)
}
if (spjObj["b"]) {
    console.log(spjObj["b"])
}

// Set的写法
let jiuyiObj2 = new Set();
jiuyiObj2.add('a')
jiuyiObj2.add('b')
if (jiuyiObj2.has("b")) {
    console.log('11', 11)
}

Set遍历操作

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

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

由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

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"]
let set = new Set([1, 2]);
set.forEach(function(value, key, ownerSet) {
    console.log(key + ": " + value);
});
// 1:1
//2:2
let a = new Set(['a', 'b', 'c']);
for (let i of a) {
    console.log('i', i)
}

Set 可以很容易地实现并集(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}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法

这意味着,可以省略values方法,直接用for...of循环遍历 Set

Set.prototype[Symbol.iterator] === Set.prototype.values
// true
let a = new Set(['a', 'b', 'c']);
for (let i of a) {
    console.log('i', i)
}
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']

ES6数组去重

let arr = [1, 2, 2, 3];
let set = new Set(arr);
let newArr = Array.from(set);
console.log(newArr); // [1, 2, 3]
// 去除数组的重复成员
[...new Set(array)]
//去除字符串里面的重复字符
[...new Set('ababbc')].join('')
// "abc"
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

Set集合转化Array数组

let set = new Set([1, 2, 3, 3, 4]);
let arr = Array.from(set)  //输出[1,2,3,4]
//
function dedupe(array) {
  return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]) // [1, 2, 3]

Set类型转换

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

//Set 实例添加了两次NaN,但是只会加入一个。这表明,在 Set 内部,两个NaN是相等的。'
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

另外,两个对象总是不相等的。

let set = new Set();

set.add({});
set.size // 1

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

遍历操作中改变原来的 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

你可能感兴趣的:(JavaScript,#,ES6,前端,javascript,es6)