重学ES6(四)Set 和 Map

180 阅读8分钟

Set

基础知识

Set 是 ES6 的新的数据结构。是类似数组的数据结构,但是里面的成员的值是唯一的。

Set 是一个构造函数,用来生成 Set 数据结构

eg1:

const s = new Set();

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

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

这个例子可以看到,Set 不会添加重复的值。

Set 可以接受数组(或者具有Iterator接口的数据)作为参数,用来初始化

eg1:

// 初始化数组数据
const s= new Set([1, 2, 3, 4, 4])
// 解构 Set 数据,发现数组内重复的数据在 Set 中没有了
[...s]		// [1, 2, 3, 4]
// size 获取 Set 的长度
s.size	// 4

可以通过上面的小demo来实现数组和字符串去重

eg2:

// 数组去重
const arr = [1, 2, 3, 4, 5, 5, 4]
[...new Set(arr)]			// [1, 2, 3, 4, 5]

// Array.from 可以将 Set 结构转为数组
Array.form(new Set(arr))	// [1, 2, 3, 4, 5]

// 字符串去重
const str = 'abbaccddeeff'
[...new Set(str)].join('')		// "abcdef"

注意:向 Set 加入值时,不会发生类型转换的,所以 1 和 "1" 是两个不同的值。Set 内部判断两个值是否不同,使用的类似完全相等运算符(===),主要区别是向 Set 加入值时认为 NaN 等于自身,而 === 运算符认为 NaN 不等于自身。

eg3:

let s = new Set()
let a = NaN
let b = NaN
s.add(a)
s.add(b)

s	// Set(1) {NaN}

NaN === NaN
false

同时,两个对象一直是不相等的

eg4:

let s = new Set()
s.add({})
s	//	Set(1) {{…}}
s.add({})
s	//	Set(2) {{…}, {…}}

Set 是一个构造函数, Set 的实例的原型上是有属性和方法的

由上图,我们可以看到 Set 构造函数上有很多属性和方法

Set 属性

Set.prototype.constructor	构造函数 Set 本身
// Set.prototype.constructor === Set   true

Set.prototype.size	 返回 Set 实例的成员总数

Set 方法

Set.prototype.add(value)	 添加值
Set.prototype.delete(value)  删除值
Set.prototype.has(value)     判断该值是否为Set成员
Set.prototype.clear()		 清理所有成员

Set 遍历操作

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

keys() values() entries()

keys() values() entries() 返回的都是遍历起对象(这个我们要在Iterator分析遍历器)。由于 Set 结构是没有键名的,只有键值(或者说键值和键名都是同一个值),所以 keys 和 values 都是返回相同的值。

eg5:

let s4 = new Set(['a', 'b', 'c'])
s4		// Set(3) {"a", "b", "c"}

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

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

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

对象的Symbol.iterator属性,指向该对象的默认遍历器方法,这个我们在 Symbol 那一节简单说过,对于详细的遍历器,我们还是在后面分析。这里我们知道 Set 结构的实例默认可以遍历,它的默认遍历器生成函数就是它的values方法。

eg6:

Set.prototype[Symbol.iterator] === Set.prototype.values
// true

从上面例子,我们就可以知道既然遍历默认的遍历器就是 values 方法,那 for...of 循环遍历 Set,就可以省略 valuse 方法

for(let item of s4) {
	console.log(item)
}
// a 
// b 
// c

forEach

Set 结构也和数组一样,拥有 forEach 方法

eg7:

let s5 = new Set([1, 4, 7])
s5.forEach((value, key, total) => {
    console.log(key + ':' + value)
    console.log(total)
})

// 1:1
// Set(3) {1, 4, 7}
// 4:4
// Set(3) {1, 4, 7}
// 7:7
// Set(3) {1, 4, 7}

根据 Set 的特性,我们很容易通过 Set 来实现 并集、交集和差集

eg8:

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

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

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

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

Set 的实现

eg1:

// 对对象的方式实现 Set 的思想
function MySet () {
	var item = {}
    this.has = function (val) {
		// in 关键词不仅仅能获取到当前对象的属性,还能判断继承的属性
    	// return val in item
        // hasOwnProperty 判断当前对象的属性,非继承
		return item.hasOwnProperty(val)
    }
    // set 数据结构是唯一的,并且 set 的 key 和 value 遍历后是一样的
    this.add = function (val) {
    	if(!this.has(val)) {
			item[val] = val
            return true
		}
        return false
    }
    // 移除对象中某个属性
    this.remove = function (val) {
		if(this.has(val)) {
        	delete item[val]
            return true
        }
        return false
	}
    // 直接将对象情况
    this.clear = function () {
    	item = {}
    }
    // 计算对象中属性的个数
    this.size = function () {
    	return Object.keys(item).length
    }
    // 获取所有的 values,实际 value 和 key 的值是一样的
    this.values = function () {
    	var values = []
        for (var val in item) {
        	if (item.hasOwnProperty(val)) {
				values.push(val)
			}
        }
        return values
    }
}

Map

基础知识

JavaScript 的对象(Object),本质上是键值对的集合(Hash结构),但是只能用字符串当作键。

Map就是为了解决这个问题而设计的。它类似于对象,也是键值对的集合,但是"键"的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Object结构提供了 字符串-值 的对应,Map提供了 值-值 的对应。

eg1:

const m = new Map()
const o = {a: 'chencc'}

m.set(o, 'test')
m.get(o)		// "test"

m.has(o)		// true
m.delete(o)		// true
m.has(o)		// false

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

eg2:

const map = new Map([
    ['name', 'chencc'],
    ['age', 25]
])

map
// Map(2) {"name" => "chencc", "age" => 25}

map.size	// 2
map.has('name')		// true
map.get('name')		// "chencc"

map 构造函数接受数组作为参数,实际上执行的是下面的算法

eg3:

const items = [
	['name', 'chencc'],
    ['age', 25]
]

const map = new Map()

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

事实上,不仅仅是数组可以这样生成 Map,任何具有 Iterator 接口,且每个成员都是双元素的数组结构,都可以当作 Map 的参数。即可以用set和map来生成新的map。

eg4:

const set = new Set([
	['name', 'chencc'],
    ['age', '25']
])

const map = new Map(set)
map		// Map(2) {"name" => "chencc", "age" => "25"}

const map1 = new Map([['name', 'chencc']])
map1	// Map(1) {"name" => "chencc"}

const map2 = new Map(map1)
map2	// Map(1) {"name" => "chencc"}

如果对同一个键多次赋值,后面的值将会覆盖前面的值,获取一个未知的键,返回undefined

eg5:

const map = new Map()

map.set('name', 'chencc').set('name', 'chenhh')
// Map(1) {"name" => "chenhh"}

map.get('hahah')	// undefined

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

const map = new Map()
m6.set({a: 1})
// Map(1) {{…} => undefined}
m6.set({a: 1})
// Map(2) {{…} => undefined, {…} => undefined}
m6.get({})
// undefined

上面代码的 set 和 get 方法,使用的是同一个键,都是同一个对象,但是这两个不同的对象实例,内存地址是不一样的。同理数组也是一样。

由此可知,Map的键是和内存地址绑定的,只要内存地址不一样,就是两个key。

Map 是一个构造函数, Map 的实例的原型上是有属性和方法的 Map 属性

Map.prototype.constructor	构造函数 Map 本身
// Map.prototype.constructor === Map   true

Map.prototype.size	 返回 Map 实例的成员总数

Map 方法

Map.prototype.set(key, value)	 添加值
Map.prototype.get(key)	 		 读取key对应的值
Map.prototype.has(key)			 判断是否存在某个键
Map.prototype.delete(key)		 删除某个key
Map.prototype.clear()			 清除所有成员

Map 遍历操作

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

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

const map = new Map([
    ['name', 'chencc'],
    ['age', '25']
])

for (let key of map.keys()) {
    console.log(key)
}
// name
// age

for (let value of map.values()){
	console.log(value)
}
// chencc
// 25

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// name chencc
// age 25

// 也可以采用解构赋值的方式取到值
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// name chencc
// age 25

// 实际是等同于 
for (let [key, value] of map) {
  console.log(key, value);
}
// name chencc
// age 25

从上面例子我们可以知道,Map结构的默认遍历器接口(Symbol.iterator)就是 entries 方法

map[Symbol.iterator] === map.entries

Map 结构转为数组结构

const map = new Map([
    ['name', 'chencc'],
    ['age', '25']
])

[...map.keys()]		// ["name", "age"]
[...map.values()]	// ["chencc", "25"]

[...map.entries()]	// [["name", "chencc"], ["age", "25"]]
[...map]			// [["name", "chencc"], ["age", "25"]]

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

Map转为数组

const map = new Map()
map.set('a', 1).set({'age': 1}, ['abc'])

[...map]
// [['a', 1], [{'age': 1}, ['abc']]]

数组转为Map

new Map([
	['a', 1], 
    [{'age': 1}, ['abc']]
])

// Map(2) {"a" => 1, {…} => Array(1)}

Map转为对象

const map = new Map()
map.set('a', 1).set('b': 2)

let obj = Object.create(null)

for (let [k, v] of map) {
	obj[k] = v
}

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

Map转为JSON

JSON 数据格式

// 对象格式
{
	key: value,
	key: value,
    key: value
}
// 数组格式
[obj, obj, obj]

根据两类不同的 JSON 格式

// Map 的键名都是字符串 通过 转为对象 JSON
const map = new Map()
map.set('a', 1).set('b': 2)

let obj = Object.create(null)

for (let [k, v] of map) {
	obj[k] = v
}

console.log(obj)
// {a: 1, b: 2}
console.log(JSON.stringify(obj))
// "{"a":1,"b":2}"

// Map 的键名有非字符串 转为数组 JSON
JSON.stringify([...map])
// "[["a",1],["b",2]]"

JSON转为Map

// JSON的键名都是字符串
const json =  "{"a":1,"b":2}"

let obj = JSON.parse(json)
// {a: 1, b: 2}

let map = new Map()
for (let key of Object.keys(obj)) {
	map.set(key, obj[k])
}
// Map(2) {"a" => 1, "b" => 2}

// 数组类型的 json
let arrJson = '[["a", "b"],[{"c": 1}, ["abc"]]]'
let arr = JSON.parse(arrJson)

let map = new Map(arr)
// Map(2) {"a" => "b", {…} => Array(1)}

Map 的实现

function Map() {
    var item = {};
    this.has = function(key){
        return key in item;
    },
    this.set = function(key,value){
        item[key] = value;
    },
    this.remove = function(key){
        if (this.has(key)) {
            delete item[key];
            return true;
        }
        return false;
    },
    this.get = function(key){
        return this.has(key)?item[key]:undefined;
    },
    this.values = function(){
        var values = [];
        for(var k in item){
            if (this.hasOwnProperty(k)) {
                values.push(item[k]);
            }
        }
        return values;
    },
    this.clear = function(){
        item = {};
    },
    this.size = function(){
        return Object.Keys(item).length;
    },
    this.getItem = function(){
        return item;
    }
}

ES5使用对象的方式模拟的 Map 是没有办法使用对象或者数组作为key的,对于模拟这个Map结构,我们主要是为了理解 Map 这个数据结构的概念,Map 作为集合的想法。

备注:其实还有 WeakSet 和 WeakMap 这两个方法,我们后面根据实际应用场景来单独分析这两个方法,大家要想先了解的,也可以去搜搜教程先看看。

总结

这里我们对于 ES6 的两个新的数据结构 Set 和 Map 进行了一系列的学习,了解 Set 数据结构的唯一性,整个 Set 中,不能存储相同的数据,但是对于引用类型,地址不同就认为是不同的数据。Map 作为集合,实现了一个类似对象的数据结构,但是所有数据类型都可以作为 key,基本了解了 Set 和 Map,我们就可以在实际项目开发中,使用 Set 和 Map 来处理数据了,大家都去试试把。