Skip to content
作者:Yuan Tang
更新于:5 个月前
字数统计:2.4k 字
阅读时长:9 分钟

第十一章 Set和Map数据结构

前面介绍完了7种数据类型,本章节阮一峰老师开始讲起了数据结构 SetMap 。这两个数据结构在开发中十分常用,并且二者都有弱引用写法。

本章节着重从它们的含义、语法、用法搭配示例入手,尽可能地详细解释说明。

Set

Set 是 ES6 引入的一种新的数据结构,类似于数组,但其中的元素值唯一,不允许重复。

基本用法

  1. 创建 Set 实例:

    使用 new Set() 创建一个空的 Set 实例。

    可以通过传入一个数组或其他可迭代对象来初始化 Set,初始化时会自动去除重复的元素。

    js
    const s = new Set() // 创建一个空的 Set 实例
    const set1 = new Set([1, 2, 3, 4, 4]) // 初始化 Set,自动去重
  2. 向 Set 添加元素:

    使用 add 方法向 Set 添加新元素,如果元素已经存在则不会重复添加。

    js
    const s = new Set();
    [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x))
    // s 现在为 Set { 2, 3, 5, 4 }
  3. 遍历 Set:

    使用 for...of 循环可以遍历 Set 中的所有元素,遍历顺序即插入顺序。

    js
    const s = new Set([2, 3, 5, 4])
    for (const i of s)
      console.log(i) // 输出 2, 3, 5, 4
  4. 获取 Set 的大小:

    使用 size 属性可以获取 Set 中元素的数量。

    js
    const items = new Set([1, 2, 3, 4, 5, 5, 5, 5])
    items.size // 输出 5
  5. 特殊情况处理:

    Set 内部使用 Same-value equality 算法判断元素是否相等,类似于 === 运算符,但是对 NaN 的处理不同。在 Set 中 NaN 是等于自身的。

    js
    const set = new Set()
    set.add(Number.NaN)
    set.add(Number.NaN)
    set // 输出 Set { NaN }

    对象是根据引用地址判断是否相等,而非对象内容。

    js
    const set = new Set()
    set.add({}) // 第一个空对象
    set.add({}) // 第二个空对象,不同的引用
    set.size // 输出 2

Set实例的属性和方法

Set 拥有 2个属性和 4种方法:

  • 属性
    1. Set.prototype.constructor:构造函数,默认是 Set 函数
    2. Set.protptype.size:返回set实例的成员数量
  • 方法
    1. add:添加某个值,返回添加后的 Set 结构
    2. delete:删除某个值,返回布尔值表示是否删除成功
    3. has:返回布尔值表示 Set 内是否存在该参数
    4. clear:清空 Set,无返回值

下面来使用一下:

js
const s = new Set()

s.add(1).add(2).add(2)

console.log(s, s.size) // [1, 2], 2

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

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

Array.from 可以把 Set 转为数组,这就提供了数组去重的方法。

遍历操作

Set 有四种遍历方法:

  • keys() 返回键名

  • values() 返回键值

  • entries() 返回键值对

    对于 Set 而言,只有键值没有键名(或者说键名与键值相同),因此 keys() 方法和 values() 方法返回的值是一样的,entries() 方法返回的数组两个值一样。

    js
    const set = new Set(['tydumpling', 'xiaodao', 'tydumpling'])
    
    for (const key of set.keys())
      console.log(key)
    
    // tydumpling
    // xiaodao
    // tydumpling
    
    for (const value of set.values())
      console.log(value)
    
    // tydumpling
    // xiaodao
    // tydumpling
    
    for (const entrie of set.entries())
      console.log(entrie)
    
    // [tydumpling, tydumpling]
    // [xiaodao, xiaodao]
    // [tydumpling, tydumpling]

    另外 Set 默认可遍历,遍历返回的值默认是 values()

  • forEach() 使用回调函数遍历每个成员

    js
    const set = new Set([1, 2, 3])
    
    set.forEach((value, key) => {
      console.log(value)
    })
    // 1
    // 2
    // 3

特别的,Set 插入顺序就是遍历的顺序,这点在做回调函数列表时非常有用,能够保证按照插入顺序依次执行。

WeakSet

含义

WeakSetSet 一样,也是不重复值的集合,但是有两点区别:

  1. 成员对象。WeakSet 的成员对象只能是对象,如果使用其他类型的值,会报错

    js
    const wset = new WeakSet()
    
    wset.add(1) // TypeError: Invalid value used in weak set.
  2. 弱引用。WeakSet 中的对象都是弱引用类型,若外部不再引用该对象,则垃圾回收器会自动回收,不考虑该对象是否还在 WeakSet 中使用。因此,ES6 规定 WeakSet 不可遍历

语法

由于 WeakSet 是一个构造函数,因此需要使用 new 创建数据结构。可以接收任何具有 iterable 接口的对象作为参数。

js
const a = new WeakSet([[1, 2], [3, 4]])

请注意,上述代码是每一项成员都是一个数组,只有数组对象可以作为 WeakSet 的成员。

js
const b = new WeakSet([1, 2]) // 报错

Set 相比,WeakSet 由于不能遍历,因此少了 size 等两种属性和 clear() 方法,只有三种方法:

  • add(value) 添加新成员
  • delete(value) 删除指定成员
  • has(value) 返回布尔值,表示是否有该值

Map

含义

ES6 之前都是使用对象 Object 做键值对的集合,但是对象有一个很大的限制,他只能使用字符串作为键名。

js
const element = document.querySelector('#content')

const obj = {}
obj[element] = 'content'

console.log(obj['[object HTMLDivElement]']) // content

上述代码是因为对象只能接受字符串,因此获取到的 DOM 元素会被 toString() 隐式转换为字符串。

ES6 新出了 Map 数据结构,类似于对象,但是键名不仅限于字符串,各种类型(包括对象)都可以作为键名。

Map 有两种添加值的方法:

  1. set() 方法添加值,get() 方法获取值,delete 方法删除值,has() 方法判断是否有值

    js
    const m = new Map()
    m.set([1, 2], 'array')
    m.get([1, 2]) // array
    m.has([1, 2]) // true
  2. 接收一个二维数组作为参数,第二维的数组第一项是键名,第二项是键值

    js
    const m = new Map([
      ['name', '张三'],
      ['age', '24']
    ])
    
    m.size // 2
    m.get('name') // 张三
    m.has('age') // true

    这个方法本质上是对传递的数组参数执行循环算法,依次 set 赋值。

    js
    const items = [
      ['name', '张三'],
      ['age', '24']
    ]
    
    const map = new Map()
    
    items.forEach(([value, key]) => {
      map.set(key, value)
    })

    不仅是数组,所有具有 Iterator 接口且每个成员都是双元素数组的数据结构都可以作为 Map 的构造函数参数。因此,SetMap 都可以用来生成新的 Map

    js
    const set = new Set([
      ['foo', 123],
      ['bar', 456]
    ])
    
    const map = new Map(set)
    
    map.get('foo') // 123
    map.get('bar') // 456
    
    const m1 = new Map([
      ['n', 1],
      ['m', 2]
    ])
    
    const m2 = new Map(m1)
    
    m2.get('n') // 1
    m2.get('n') // 2

⚠️ 注意

对于基本数据类型,只要值相同即可(需要注意的是,0 和 -0 在 Map 中是相同的;NaN 不严格等于自身,但在 Map 中视为相同的)

js
// 基本数据类型
const mm = new Map()

mm.set(0, '0')
mm.get(-0) // '0'

mm.set(Number.NaN, 123)
mm.get(Number.NaN) // 123

对于数组和对象这类复杂数据类型,Map 结构将其内存地址作为是否同一个键的比较标准

js
const map = new Map()

map.set(['a'], 1)
map.get(['a']) // undefined

const arr = ['b']
map.set(arr, 'array')
map.get(arr) // array

实例属性与语法

  • 属性

    size 属性可以获取 Map 成员的总数

    js
    const map = new Map([
      ['a', 1],
      ['b', 2],
      ['c', 3]
    ])
    
    map.size // 3
  • 方法

set(key, value) 方法设置 key 所对应的键值对,并返回总的 Map 结构。可以采取链式写法。

js
const m = new Map()
  .set(1, 'tydumpling')
  .set('2', 'xiaodao')
  .set(undefined, 'tydumpling')

get(key) 方法获取 key 对应的键值,找不到返回 undefined

js
const m = new Map()

function sayHi() {
  console.log('hello')
}

m.set('hi', sayHi)

m.get('hi') // function
m.get('hello') // undefined

has(key) 方法返回布尔值,表示该键值是否在 Map 中有数据。

delete(key) 方法删除 Map 中某个键,删除成功返回 true ,反之返回 false

clear() 方法清空所有成员,无返回值。

遍历操作

Set 一致,Map 也拥有 keys()values()entries()forEach() 四种遍历方法,遍历的顺序也是插入顺序。

js
const map = new Map([
  [1, 'a'],
  [2, 'b'],
  [3, 'c'],
])

for (const k of map.keys())
  console.log(k)

// 1
// 2
// 3

for (const k of map.values())
  console.log(k)

// a
// b
// c

for (const k of map.entries())
  console.log(k[0], k[1])

// 1, a
// 2, b
// 3, c

Map 可以通过拓展运算符 ... 实现快速转数组操作,如转为数组、使用 mapfilter 执行操作等。

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

const arr = [...map] // [[1, 'a'], [2, 'b'], [3, 'c']]

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

const filterMap = new Map([...map].filter(([key, value]) => value !== 'c')) // Map(2) {1 => 'a', 2 => 'b'}

const mapMap = new Map([...map].map(([key, value]) => [key * 2, `${value}-`])) // Map(3) {2 => 'a-', 4 => 'b-', 6 => 'c-'}

此外,Map 可以使用 forEach 方法实现遍历操作,使用方式和数组一致。

js
const map = new Map([
  [1, 'a'],
  [2, 'b']
])

map.forEach((value, key, item) => {
  console.log(value, key, item) // a, 1, [1, 'a']
})

Map 可以接收第二个参数,用于绑定 this

js
const map = new Map([
  [1, 'a'],
  [2, 'b']
])

const ojb = {
  sayLog: (v, k, e) => {
    console.log(v, k, e)
  }
}

map.forEach((value, key, item) => {
  this.sayLog(value, key, item)
}, ojb)

数据结构转换

  • Map 转数组

    上文也提到,Map 转数组只需要拓展运算符 ... 即可。

    js
    const map = new Map([
      [1, 'a'],
      [2, 'b']
    ])
    
    console.log([...map]) // [[1, 'a'], [2, 'b']]
  • 数组转 Map

    数组放到 new Map 的括号内就能转为 Map

    js
    const arr = [
      [1, 'a'], [2, 'b']
    ]
    
    const map = new Map(arr)
  • Map 转对象

    如果 Map 的每一个键都是字符串的形式,则可以通过代码逻辑转为 Map

    js
    const map = new Map([
      ['yes', true],
      ['no', false]
    ])
    
    function toObj(params) {
      const obj = Object.create(null)
      for (const [k, v] of params)
        obj[k] = v
    
      return obj
    }
    
    console.log(toObj(map)) // {yes: true, no: false}
  • 对象转 Map

    同理可得,可以通过代码逻辑把对象转为 Map

    js
    const obj = {
      yes: true,
      no: false
    }
    
    function toMap(params) {
      const map = new Map()
      for (const k of Object.keys(params))
        map.set(k, params[k])
    
      return map
    }
    
    console.log(toMap(obj)) // Map(2) {'yes' => true, 'no' => false}
  • MapJSON

    MapJSON 有两种情况:

    • 键都是字符串型:此时可以转为对象形式的 JSON

      js
      function toObjJSON(params) {
        return JSON.stringify(toObj(params)) // 使用上方`Map` 转对象中的方法
      }
      
      const map = new Map([
        ['a', 1],
        ['b', 2]
      ])
    • 键不全是字符串型:此时转为数组形式的 JSON

  • JSONMap

WeakMap

Contributors

Yuan Tang