跳转到内容

第十一章 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 (let 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 的处理不同。在 SetNaN 是等于自身的。

    js
    let set = new Set();
    set.add(NaN);
    set.add(NaN);
    set; // 输出 `Set` { NaN }

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

    js
    let 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
    let set = new Set(['daodao', 'xiaodao', 'duyidao'])
    
    for (let key of set.keys()) {
      console.log(key)
    }
    // daodao
    // xiaodao
    // duyidao
    
    for (let value of set.values()) {
      console.log(value)
    }
    // daodao
    // xiaodao
    // duyidao
    
    for (let entrie of set.entries()) {
      console.log(entrie)
    }
    // [daodao, daodao]
    // [xiaodao, xiaodao]
    // [duyidao, duyidao]

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

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

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

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

WeakSet

含义

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

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

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

语法

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

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

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

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

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

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

Map

含义

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

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

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

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

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

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

Map 有两种添加值的方法:

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

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

    js
    let 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
    let set = new Set([
      ['foo', 123],
      ['bar', 456]
    ])
    
    let map = new Map(set)
    
    map.get('foo') // 123
    map.get('bar') // 456
    
    let m1 = new Map([
      ['n', 1],
      ['m', 2]
    ])
    
    let m2 = new Map(m1)
    
    m2.get('n') // 1
    m2.get('n') // 2

⚠ 注意

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

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

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

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

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

js
let map = new Map()

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

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

实例属性与语法

  • 属性

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

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

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

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

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

js
const m = new Map()

const 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 (let k of map.keys()) {
  console.log(k)
}
// 1
// 2
// 3

for (let k of map.values()) {
  console.log(k)
}
// a
// b
// c

for (let k of map.entries()) {
  console.log(k[0], k[1])
}
// 1, a
// 2, b
// 3, c

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

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

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

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

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

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

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

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

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

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

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

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

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

数据结构转换

  • Map 转数组

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

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

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

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

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

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

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

    js
    let obj = {
      yes: true,
      no: false
    }
    
    const toMap = params => {
      let map = new Map()
      for (let 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` 转对象中的方法
      }
      
      let map = new Map([
        ['a', 1],
        ['b', 2]
      ])
    • 键不全是字符串型:此时转为数组形式的 JSON

  • JSONMap

    JSON 转为 Map ,正常情况下所有键名都是字符串

    js
    function jsonToStrMap(jsonStr) { 
      return objToStrMap (JSON .parse(jsonStr) ); 
    }
    jsonToStrMap('{"yes": true, "no": false }') // Map {’ yes ’=> true,’no’ => false}

    但是,有 种特殊情况:整个 JSON 就是 个数组,且每个数组成员本身又是 个具有两个成员的数组。这时,它可以一一对应地转为 Map 。这往往是数组转为 JSON 的逆操作。

    js
    function jsonToMap(jsonStr) { 
      return new Map(JSON.parse(jsonStr))
    }
    jsonToMap('[[true, 7], [{"foo": 3}, ["abc"]]]') // Map {true=> 7 , Object {foo: 3) => [’ abc ’ ]}

WeakMap

含义

WeakMapMap 相似,都是用 set() 生成键值对集合,get() 获取值,has() 判断是否存在,delete 删除键值对。

两者的区别在于:

  1. WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。如果用 numberstring 等类型作为键名,会报错。

  2. WeakMap 的键名所指向的对象,不计入垃圾回收机制。

    垃圾回收机制是判断当前变量是否被使用到,如果被使用到就不会被回收,因此有时候如果使用了某些数据变量用完后没及时置空,则这些变量会一直占用内存,导致内存泄漏。

    js
     let obj1 = {
       a: 1,
       b: 2
     }
    
     let arr = [obj1, 23]

    WeakMap 的键名是弱引用,垃圾回收机制会自动回收键名所指向的对象,因此不会造成内存泄漏。变量被垃圾回收后, WeakMap 中对应的键值对也会自动消失,无法访问。

⚠ 注意

WeakMap 引用的只是键名而不是键值,键值依然是正常引用的。

语法

由于 WeakMap 是弱类型引用,因此某个键名是否存在完全不可预测,可能上一秒还存在,下一秒对象被垃圾回收机制运行回收了,消失找不到了。为了防止这种不确定性, ES6 规定, WeakMap 没有遍历操作,也没有 size 属性,也不支持 clear 方法。因此, WeakMap 只能用来存储键值对,而不能用来存放任何复杂的数据结构。

用途

WeakMap 的用途主要是用来存储私有数据,因为它的键名是弱引用,垃圾回收机制会自动回收键名所指向的对象,因此不会造成内存泄漏。

  1. WeakMap 可以用来存储 DOM 节点的私有属性,防止内存泄漏。
    js
    let myElement = document.getElementById('logo')
    let myWeakmap = new WeakMap()
    
    myWeakmap.set(myElement, handler)
    
    myElement.addEventListener('click', myWeakmap.get(myElement), false)
  2. WeakMap 可以用来部署私有属性。
js
 const _counter= new WeakMap(); 
  const _action= new WeakMap(); 
  class Countdown {
    constructor(counter, action) { 
      counter.set(this, counter); 
      action.set (this, action);
    }
    dec() { 
      let counter= _counter.get(this); 
      if (counter < 1) return ; 
        counter--; 
        _counter.set(this, counter) ; 
        if (counter === 0) {
          _action.get(this) ();
        }
      }
  }
  const c = new Countdown(2, () => console.log('DONE ')); 
  c.dec() 
  c.dee()

总结

ES6中新增了两种数据结构:SetMap,以及它们的弱引用版本 WeakSetWeakMapSet 是一个元素唯一、无序且允许重复的数据结构,通过 new Set() 创建,支持 adddeletehas 等方法,并可通过 for...of 循环遍历。Map 是键值对的集合,键不仅限于字符串,支持 setgethasdelete 等方法,同样可通过 for...of 循环遍历。WeakSetWeakMap 分别对应 SetMap 的弱引用版本,只接受对象作为键,且键的引用为弱引用,有利于垃圾回收,但不可遍历且没有 size 属性。

SetMap 的遍历操作包括 keys()values()entries()forEach(),保持插入顺序。Array.from() 可将 Set 转为数组,实现去重。Map 可以转换为数组,数组也可以转换为 Map ,两者互为逆操作。WeakMap 用于存储私有数据,防止内存泄漏,适用于存储 DOM 节点的私有属性和部署私有属性。