learn-typescript

Set 和 Map

Set

SetES6 提供的一种新的数据结构。类似于数组,但是不允许有重复值(因为键名和键值相同)。

使用 Set() 构造函数可以生成 Set 类型数据结构:

let set = new Set()
;[1, 2, 2, 3, 2, 1].map(item => set.add(item))

for (let item of set) {
  console.log(item) // 1 2 3
}

Set() 构造函数可以接受一个具有 iterator 接口的数据结构作为参数(例如数组),用来初始化:

let set: Set<any> = new Set([1, 2, 3])
console.log(set) // Set { 1, 2, 3 }

set = new Set('abc')
console.log(set) // Set { 'a', 'b', 'c' }

由于 Set 中的值的唯一性,所以可以用来实现数组去重:

function dedupe(arg: any[] | string) {
  return [...new Set(arg)]
}
console.log(dedupe('aabacdcdd')) // [ 'a', 'b', 'c', 'd' ]
console.log(dedupe([1, 1, 2, 3, 3, 2, 4])) // [ 1, 2, 3, 4 ]

Set 内部判断两个值是否相同,使用的算法叫做 "Same-value-zero equality" ,类似于精确相等运算符(===),主要区别是 Set 中加入 NaN 只能加入一次,而精确相等运算符(===)认为 NaN !== NaN

let se = new Set()
se.add(NaN)
se.add(NaN)
console.log(se) // Set { NaN }

注意:两个对象总是不相等的。这点与 === 一致。

let set = new Set()
set.add({})
set.add({})
console.log(set) // Set { {}, {} }
console.log({} === {}) // false

Set 实例的属性和方法

属性

方法

Set 的实例方法分为两类: 操作方法 和 遍历方法:

  1. 操作方法

    • Set.prototype.add(value): 添加某个值,返回 Set 实例本身
    • Set.prototype.delete(value): 删除某个值,返回布尔值表示是否删除成功
    • Set.prototype.has(value): 返回布尔值,表示是否有该成员
    • Set.prototype.clear(): 清除所有成员,没有返回值
    let se = new Set([2, 3])
    se.add(1)
    console.log(se.size) // 3
    console.log(se.delete(2)) // true
    console.log(se.has(2)) // false
    se.clear()
    console.log(se) // Set {}
    
  2. 遍历方法

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

    Set 的遍历顺序就是按照插入的顺序。

    1. keys(), values(), entires() 这三个方法均返回遍历器,由于 Set 键名与键值一样,所以 keys()values() 效果一致:

      let set = new Set(['red', 'blue', 'green'])
      for (const item of set.keys()) {
        console.log(item) // red blue green
      }
      
      for (const item of set.values()) {
        console.log(item) // red blue green
      }
      
      for (const item of set.entries()) {
        console.log(item)
      }
      // [ 'red', 'red' ]
      // [ 'blue', 'blue' ]
      // [ 'green', 'green' ]
      

      Set 实例默认可以遍历,它的默认遍历器([Symbol.iterator])就是它的 values() 方法:

      Set.prototype[Symbol.iterator] === Set.prototype.values // true
      
      for (const item of set) {
        console.log(item) // red blue green
      }
      
    2. forEach() 用于对每个成员进行某种操作:

      set.forEach((value, key) => {
        console.log(key, value)
      })
      // red red
      // blue blue
      // green green
      
    3. 遍历的应用

      拓展运算符 ... 内部使用 for...of 循环,所以也可以用于 Set 结构:

      let set = new Set([1, 2, 3, 2, 3, 1])
      let arr = [...set]
      console.log(arr) // [ 1, 2, 3 ]
      

      Set 可以很容易实现 并集, 交集差集

      let setOne = new Set([1, 2, 3])
      let setTwo = new Set([4, 3, 2])
      
      // 并集
      let union = new Set([...setOne, ...setTwo]) // Set { 1, 2, 3, 4 }
      // 交集
      let intersect = new Set([...setOne].filter(item => setTwo.has(item))) // Set { 2, 3 }
      // 差集
      let difference = new Set([...setOne].filter(item => !setTwo.has(item))) // Set { 1 }
      

WeakSet

WeakSetSet 类似,但是有两个区别:

  1. WeakSet 的成员只能是对象(不包括 null),不能是其他类型的值:

    let ws = new WeakSet()
    ws.add({}) // ok
    ws.add(1) // Error,只能存放对象成员
    ws.add(Symbol()) // Error,只能存放对象成员
    ws.add(null) // Error,只能存放对象成员
    
  2. WeakSet 中的对象都是 弱引用,垃圾回收机制不考虑 WeakSet 对该对象的引用,如果其他对象都不再引用该对象,该对象的内存将被回收, WeakSet 中该对象成员也将消失。正是由于这个特点,ES6 规定 WeakSet 无法被遍历,也没有 size 属性。

    垃圾回收机制依赖于 引用计数,如果一个对象的引用次数不为 0,则无法被回收,如果结束使用某个对象之后忘记取消引用,导致内存无法释放,则会引起 内存泄漏WeakSet 中的引用属于弱引用,不计入垃圾回收机制,所以不存在这个问题。因此,它是用于临时存放一组对象。

语法

  1. 构造方法

WeakSet()构造方法类似 Set()构造方法,但是传入的 iterable 参数的成员必须是对象:

let arr1 = ['a']
let arr2 = [1]
let ws = new WeakSet([arr1, arr2])
  1. 方法 WeakSet 没有遍历操作(即没有 keys() , values(), entries(), forEach()),也没有 size 属性。
    另外 WeakSet 无法被清空,即没有 clear() 方法。
const ws = new WeakSet()
const obj = {}
const foo = {}
const bar = {}

ws.add(obj)
ws.add(foo)

console.log(ws.has(obj)) // true
console.log(ws.has(bar)) // false

console.log(ws.delete(obj)) // true
console.log(ws.has(obj)) // false

Map

ES6 提供的 Map 数据结构,类似于对象,但是键不限于字符串,各种类型的值(包括对象)都可以作为键,实现了 值 - 值 对应。

let m = new Map()

let o = { o: 'abc' }
m.set(o, 'hello')
console.log(m.get(o)) // "hello"

Map() 构造函数还接受一个具有 iterator 接口数据结构、且每个成员都是一个双元素数组的数据结构作为参数,符合条件的数组,Set,Map 都可以。

  1. 数组作为构造函数的参数:

    const obj1 = { a: 'A' }
    const arrA: [any, any][] = [
      [12, 'arrA'],
      ['name', 21],
      [obj1, obj1.toString()]
    ]
    
    let m1 = new Map(arrA)
    console.log(m1) // Map { 12 => 'arrA', 'name' => 21, { a: 'A' } => '[object Object]' }
    
  2. Set 作为构造函数的参数:

    const obj1 = { a: 'A' }
    const arrA: [any, any][] = [
      [12, 'arrA'],
      ['name', 21],
      [obj1, obj1.toString()]
    ]
    
    const setC = new Set(arrA)
    let m2 = new Map(setC)
    console.log(m2) // Map { 12 => 'arrA', 'name' => 21, { a: 'A' } => '[object Object]' }
    
  3. Map 作为构造函数的参数:

    const obj1 = { a: 'A' }
    const arrA: [any, any][] = [
      [12, 'arrA'],
      ['name', 21],
      [obj1, obj1.toString()]
    ]
    
    let m1 = new Map(arrA)
    let m3 = new Map(m1)
    console.log(m3) // Map { 12 => 'arrA', 'name' => 21, { a: 'A' } => '[object Object]' }
    

Map 中对同一个键多次赋值将导致后面的值覆盖前面的值。 Map 中键的相等判断方法与 Set 中判断方法相同。

特别需要注意的是:如果以对象作键名,则只有对同一个对象的引用,Map 才将其视为同一个键:

let m = new Map()
let obj = [1]

m.set([1], 'A')
console.log(m.get(obj)) // undefined
m.set(obj, 'B')
console.log(m.get(obj)) // "B"

Map 实例的属性和方法

属性

方法

Map 实例的方法同样分为 操作方法 和 遍历方法:

  1. 操作方法:

    • Map.prototype.set(key, value): 向 Map 结构中设置键值对,如果已有该键则更新值,否则新建该键值对,返回 Map 实例本身
    • Map.prototype.get(key): 获取键对应的值,如果没有则返回 undefined
    • Map.prototype.has(key): 返回一个布尔值,表示该键是否在 Map 中
    • Map.prototype.delete(key): 删除键所对应的成员,返回布尔值表示是否成功
    • Map.prototype.clear(): 清除所有成员
    let map = new Map()
    map.set(1, 'A').set('a', 2)
    console.log(map.size) // 2
    console.log(map.has(1)) // true
    console.log(map.get(1)) // "A"
    console.log(map.delete('a')) // true
    console.log(map) // Map { 1 => 'A' }
    map.clear()
    console.log(map) // Map {}
    
  2. 遍历方法:
    Map 的遍历顺序就是插入顺序。

    • Map.prototype.keys(): 返回键名的遍历器
    • Map.prototype.values(): 返回键值的遍历器
    • Map.prototype.entries(): 返回键值对的遍历器
    • Map.prototype.forEach(): 使用回调函数遍历每个成员
    let map = new Map()
    map.set(1, 'A').set('a', 2)
    
    for (const key of map.keys()) {
      console.log(key) // 1 "a"
    }
    
    for (const value of map.values()) {
      console.log(value) // "A" 2
    }
    
    for (const item of map.entries()) {
      console.log(item[0] + ' -- ' + item[1])
    }
    // "1 -- A"
    // "a -- 2"
    
    map.forEach((value, key, map) => {
      console.log(key + ' -- ' + value)
    })
    // "1 -- A"
    // "a -- 2"
    

    Map 实例默认可以遍历,它的默认遍历器([Symbol.iterator])就是它的 entries() 方法:

    Map.prototype[Symbol.iterator] === Map.prototype.entries // true
    
    for (const item of map) {
      console.log(item[0], item[1])
    }
    // 1 "A"
    // "a" 2
    

Map 与其他数据结构转换

  1. Map 转数组
    Map 转数组最便捷的方法是使用拓展运算符 ...

    let map = new Map()
    map.set({}, 'A').set({}, 1)
    console.log([...map]) // [ [ {}, 'A' ], [ {}, 1 ] ]
    
  2. 数组转 Map
    将数组传入 Map 构造函数即可:

    const obj1 = { a: 'A' }
    const arrA: [any, any][] = [
      [12, 'arrA'],
      ['name', 21],
      [obj1, obj1.toString()]
    ]
    
    let m1 = new Map(arrA)
    console.log(m1) // Map { 12 => 'arrA', 'name' => 21, { a: 'A' } => '[object Object]' }
    

WeakMap

WeakMapMap 的关系和 WeakSetSet 的关系类似:

  1. WeakMap 只接受对象作为键名(不包括 null):

    let wm = new WeakMap()
    wm.set(1, 1) // 错误
    wm.set(Symbol(), 1) // 错误
    wm.set(null, 1) // 错误
    wm.set({}, 1) // ok
    
  2. WeakMap 键名所引用的对象是 弱引用,一旦该对象的其他引用被清除, WeakMap 中该键名对应的成员自动移除。基本上想要王对象上添加数据,但是又不想干扰垃圾回收机制,就可以使用 WeakMap

WeakMap 的语法

WeakMap 相比 Map 的 API,没有了遍历方法(keys(), values(), entries(), forEach())。
另外,WeakMap 无法被清空,即没有了 clear() 方法。所以只有 4 个方法可用: set(), get(), has(), delete