ES6中的Set、Map和解构赋值
::: tip
开始讲之前我们通过几个案例,来对比ES5来看看ES6+语法新特性。
:::
💬案例1:可选链
读取一个被连接对象的深层次的属性的值
const user = {
address: {
street: '深圳市南山区xx街道',
getNum() {
return '80号'
}
}
}
ES5语法:
const street = user && user.address && user.address.street
const num = user && user.address && user.address.getNum && user.address.getNum()
ES6+语法:
const street = user?.address?.street
const num = user?.address?.getNum?.()
💬案例2:消除魔术字符串
魔术字符串指的是在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
ES5语法:
function getArea(shape) {
let area = 0
switch (shape) {
case 'Triangle':
area = 1
break
case 'Circle':
area = 2
break
}
return area
}
getArea('Triangle')
上面代码中,字符串Triangle和Circle就是魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
使用Symbol就可以很好的解决这个问题:
ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
ES6+语法:
const shapeType = {
triangle: Symbol(),
circle: Symbol()
}
function getArea(shape) {
let area = 0
switch (shape) {
case shapeType.triangle:
area = 1
break
case shapeType.circle:
area = 2
break
}
return area
}
getArea(shapeType.triangle)
💬一、Set
- 场景
在 JavaScript 里通常使用 Array 或 Object 来存储数据。但是在频繁操作数据的过程中查找或者统计并需要手动来实现,并不能简单的直接使用。 比如如何保证 Array 是去重的,如何统计 Object 的数据总数等,必须自己去手动实现类似的需求,不是很方便。 在 ES6 中为了解决上述痛点,新增了数据结构 Set 和 Map,它们分别对应传统数据结构的“集合”和“字典”。首先,我们先来简单了解下 Set 数据结构。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
基本语法 生成 Set 实例
let s = new Set()
可以定义一个空的 Set 实例,也可以在实例化的同时传入默认的数据。
let s = new Set([1, 2, 3, 4])
::: warning
初始化的参数必须是可遍历的,可以是数组或者自定义遍历的数据结构。
:::
添加数据
s.add('hello')
s.add('goodbye')
OR
s.add('hello').add('goodbye')
::: warning
Set 数据结构不允许数据重复,回自动去重的,所以添加重复的数据是无效的
:::
删除数据
删除数据分两种,一种是删除指定的数据,一种是删除全部数据。
// 删除指定数据
s.delete('hello') // true
// 删除全部数据
s.clear()
统计数据
Set 可以快速进行统计数据,如数据是否存在、数据的总数。
// 判断是否包含数据项,返回 true 或 false
s.has('hello') // true
// 计算数据项总数
s.size // 2
数组去重
let arr = [1, 2, 3, 4, 2, 3, 1]
let s = new Set(arr)
console.log(s) // Set(4) {1, 2, 3, 4}
合并去重
let arr1 = [1, 2, 3, 4]
let arr2 = [2, 3, 4, 5, 6]
let s = new Set([...arr1, ...arr2])
console.log(s) // Set(6) {1, 2, 3, 4, 5, …}
console.log([...s]) // [1, 2, 3, 4, 5, 6]
console.log(Array.from(s)) // [1, 2, 3, 4, 5, 6]
交集
let s1 = new Set(arr1)
let s2 = new Set(arr2)
let result = new Set(arr1.filter(item => s2.has(item)))
console.log(Array.from(result))
差集
let arr3 = new Set(arr1.filter(item => !s2.has(item)))
let arr4 = new Set(arr2.filter(item => !s1.has(item)))
console.log(arr3)
console.log(arr4)
console.log([...arr3, ...arr4])
遍历方式
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
- for…of:可以直接遍历每个成员
console.log(s.keys()) // SetIterator {"hello", "goodbye"}
console.log(s.values()) // SetIterator {"hello", "goodbye"}
console.log(s.entries()) // SetIterator {"hello" => "hello", "goodbye" => "goodbye"}
s.forEach(item => {
console.log(item) // hello // goodbye
})
for (let item of s) {
console.log(item)
}
for (let item of s.keys()) {
console.log(item)
}
for (let item of s.values()) {
console.log(item)
}
for (let item of s.entries()) {
console.log(item[0], item[1])
}
WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
WeakSet 的成员只能是对象,而不能是其他类型的值。
const ws = new WeakSet()
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
let ws = new WeakSet()
const obj1 = {
name: 'imooc'
}
const obj2 = {
age: 5
}
ws.add(obj1)
ws.add(obj2)
ws.delete(obj1)
console.log(ws)
console.log(ws.has(obj2))
WeakSet 没有size属性,没有办法遍历它的成员。
WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
::: tip
MDN Set: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Set
:::
💬二、Map
- 场景
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
基本语法 实例化
let map = new Map()
添加数据
let keyObj = {}
let keyFunc = function() {}
let keyString = 'a string'
// 添加键
map.set(keyString, "和键'a string'关联的值")
map.set(keyObj, '和键keyObj关联的值')
map.set(keyFunc, '和键keyFunc关联的值')
删除数据
删除数据分两种,一种是删除指定的数据,一种是删除全部数据。
// 删除指定的数据
map.delete(keyObj)
// 删除所有数据
map.clear()
统计数据
// 统计所有 key-value 的总数
console.log(map.size) //2
// 判断是否有 key-value
console.log(map.has(keyObj)) // true
查询数据
get() 方法返回某个 Map 对象中的一个指定元素
console.log(map.get(keyObj)) // 和键keyObj关联的值
遍历方式
- keys() 返回一个新的 Iterator 对象。它包含按照顺序插入 Map 对象中每个元素的 key 值
- values() 方法返回一个新的 Iterator 对象。它包含按顺序插入Map对象中每个元素的 value 值
- entries() 方法返回一个新的包含 [key, value] 对的 Iterator ? 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同
- forEach() 方法将会以插入顺序对 Map 对象中的每一个键值对执行一次参数中提供的回调函数
- for…of 可以直接遍历每个成员
map.forEach((value, key) => console.log(value, key))
for (let [key, value] of map) {
console.log(key, value)
}
for (let key of map.keys()) {
console.log(key)
}
for (let value of map.values()) {
console.log(value)
}
for (let [key, value] of map.entries()) {
console.log(key, value)
}
Map和Object一些区别
- 键的类型
一个Object的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值,包括函数、对象、基本类型。 - 键的顺序
Map 中的键值是有序的,而添加到对象中的键则不是。因此,当对它进行遍历时,Map 对象是按插入的顺序返回键值。 - 键值对的统计
你可以通过 size 属性直接获取一个 Map 的键值对个数,而 Object 的键值对个数只能手动计算。 - 键值对的遍历
Map 可直接进行迭代,而 Object 的迭代需要先获取它的键数组,然后再进行迭代。 - 性能
Map 在涉及频繁增删键值对的场景下会有些性能优势。
WeakSet
WeakMap结构与Map结构类似,也是用于生成键值对的集合。
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap()
const key = {
foo: 1
}
wm1.set(key, 2)
wm1.get(key) // 2
// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3]
const k2 = [4, 5, 6]
const wm2 = new WeakMap([
[k1, 'foo'],
[k2, 'bar']
])
wm2.get(k2) // "bar"
WeakMap与Map的区别有两点。
- WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
- WeakMap的键名所指向的对象,不计入垃圾回收机制。
const map = new WeakMap()
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
::: tip
MDN Map: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map
:::
💬三、解构赋值 Desctructuring
在 ES6 中新增了变量赋值的方式:解构赋值。允许按照一定模式,从数组和对象中提取值,对变量进行赋值。如果对这个概念不了解,我们可以快速展示一个小示例一睹风采:
ES5语法:
let arr = [1, 2, 3]
let a = arr[0]
let b = arr[1]
let c = arr[2]
想从数组中找出有意义的项要单独赋值给变量,在 ES6 中就可以这样写了:
let [a, b, c] = [1, 2, 3]
::: tip
解构赋值重点是在赋值,赋值的元素是要拷贝出来赋值给变量,赋值的元素本身是不会被改变的。
:::
是不是非常的简洁,在解构赋值里用的最多的就是 Object 和 Array ,我们可以分别来看下两者的解构赋值是如何操作的。
数组(Array)解构赋值
- 赋值元素可以是任意可遍历的对象
赋值的元素不仅是数组,它可以是任意可遍历的对象
let [a, b, c] = "abc" // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3])
- 左边的变量
被赋值的变量还可以是对象的属性,不局限于单纯的变量。
let user = {}
[user.firstName, user.secondName] = 'Liu Jay'.split(' ')
console.log(user.firstName) // Liu
console.log(user.secondName)// Jay
- 循环体
解构赋值在循环体中的应用,可以配合 entries 使用。
let user = {
name: 'John',
age: 30
}
// loop over keys-and-values
for (let [key, value] of Object.entries(user)) {
console.log(`${key}:${value}`)
}
// name:John
// age:30
当然,对于 map 对象依然适用:
let user = new Map()
user.set('name', 'John')
user.set('age', '30')
for (let [key, value] of user.entries()) {
console.log(`${key}:${value}`)
// name:John
// age:30
}
- 可以跳过赋值元素
如果想忽略数组的某个元素对变量进行赋值,可以使用逗号来处理。
// second element is not needed
let [name, , title] = ['John', 'Jim', 'Sun', 'Moon']
console.log( title ) // Sun
- rest 参数
let [name1, name2, ...rest] = ["LaoWang", "XiaoMing", "XiaoZhao", "of the Roman Republic"]
console.log(name1) // LaoWang
console.log(name2) // XiaoMing
// Note that type of `rest` is Array.
console.log(rest[0]) // XiaoZhao
console.log(rest[1]) // of the Roman Republic
console.log(rest.length) // 2
::: tip
我们可以使用 rest 来接受赋值数组的剩余元素,不过要确保这个 rest 参数是放在被赋值变量的最后一个位置上。
:::
- 默认值
如果数组的内容少于变量的个数,并不会报错,没有分配到内容的变量会是 undefined。
let [firstName, surname] = []
console.log(firstName) // undefined
console.log(surname) // undefined
为了防止 undefined 的情况出现,我们可以给变量赋予默认值。
// default values
let [name = "WangZhixun", surname = "LiuJay"] = ["LiChuang"]
console.log(name) // LiChuang (from array)
console.log(surname) // LiuJay (default used)
对象(Objct)解构赋值
- 基本用法
解构赋值除了可以应用在 Array,也可以应用在 Object。基本的语法如下:
let {var1, var2} = {var1:…, var2…};
这个大致意思是我们有一个 Object 想把里面的属性分别拿出来而无需通过调用属性的方式赋值给指定的变量。具体的做法是在赋值的左侧声明一个和 Object 结构等同的模板,然后把关心属性的 value 指定为新的变量即可。
let options = {
title: "Menu",
width: 100,
height: 200
}
let {title, width, height} = options
console.log(title) // Menu
console.log(width) // 100
console.log(height) // 200
::: tip
在这个结构赋值的过程中,左侧的“模板”结构要与右侧的 Object 一致,但是属性的顺序无需一致。
:::
上面这种写法其实就是类似于:
let {title: title, width: width, height: height} = options
也可以直接进行赋值,赋值的过程就是指定了默认值:
let options = {
title: "Menu"
}
let {width = 100, height = 200, title} = options
console.log(title) // Menu
console.log(width) // 100
console.log(height) // 200
- rest 运算符
如果我们想象操作数组一样,只关心指定的属性,其他可以暂存到一个变量下,这就要用到 rest 运算符了
let options = {
title: "Menu",
height: 200,
width: 100
}
let {title, ...rest} = options
// now title="Menu", rest={height: 200, width: 100}
console.log(rest.height) // 200
console.log(rest.width) // 100
- 嵌套对象
如果一个 Array 或者 Object 比较复杂,它嵌套了 Array 或者 Object,那只要被赋值的结构和右侧赋值的元素一致就好了。
let options = {
size: {
width: 100,
height: 200
},
items: ["金证", "前海"],
extra: true // 我们不去改变的属性
}
// 为了代码清晰进行赋值操作
let {
size: { // 左侧模板
width,
height
},
items: [item1, item2], // 对item项进行赋值
title = 'Menu' // 不存在于对象中(使用默认值)
} = options
console.log(title) // Menu
console.log(width) // 100
console.log(height) // 200
console.log(item1) // 金证
console.log(item2) // 前海
字符串解构赋值
可以当做是数组的解构:
let str = 'imooc'
let [a, b, c, d, e] = str
console.log(a, b, c, d, e)
:::tip
- 有时候我们写一个函数需要传入很多参数,而且很多参数是可选的,是否可以利用解构赋值来简化操作呢?
- 如何在业务开发中对接口数据进行解构赋值呢?
:::