深入浅出JS红宝书 - 集合引用类型(下)

922 阅读9分钟

Map

Map的大多数特性都可以通过Object类型实现,但二者之间还是存在一些细微的差异。

Object的区别

  1. "键"的类型

    • Object只能使用数值、字符串或符号作为键
    • Map可以使用任何JavaScript数据类型作为键
  2. Map实例会维护键值对的插入顺序

  3. 性能方面

    1. 内存占用

      • Map大约可以比Object多存储50%的键/值对
    2. 插入性能

      • Map的性能更佳
    3. 查找速度

      • 大型 -- 差异极小

      • 小型 -- Object有时更快

        [注]  在把Object当成数组使用的情况下(比如使用连续整数作为属性)

    4. 删除性能

      • 使用delete删除Object属性的性能差
      • Mapdelete()操作都比插入和查找更快

基本API

  • 使用嵌套数组初始化

    const m1 = new Map([
      ["key1", "val1"],
      ["key2", "val2"],
      ["key3", "val3"]
    ])
    m1  // Map {"key1" => "val1", "key2" => "val2", "key3" => "val3"}
    
  • 使用自定义迭代器初始化

    const m2 = new Map({
      [Symbol.iterator]: function*() {
        yield ["key1", "val1"];
        yield ["key2", "val2"];
        yield ["key3", "val3"];
      }
    });
    m2  // Map {"key1" => "val1", "key2" => "val2", "key3" => "val3"}
    
  • 使用set()方法添加键/值对

    const demo_map = new Map()
    demo_map.set("key1", "val1")
            .set("key2", "val2")
    demo_map  // Map {"key1" => "val1", "key2" => "val2"}
    
  • 使用get()has()进行查询,通过size属性获取长度

    const demo_map = new Map([
      ["key1", "val1"],
      ["key2", "val2"]
    ])
    demo_map.has("key1")  // true
    demo_map.get("key2")  // "val2"
    demo_map.size         // 2
    
  • 使用delete()clear()删除值

    const demo_map = new Map([
      ["key1", "val1"],
      ["key2", "val2"],
      ["key3", "val3"]
    ])
    demo_map  // Map {"key1" => "val1", "key2" => "val2", "key3" => "val3"}// delete() 删除指定项
    demo_map.delete("key1")   
    demo_map  // Map {"key2" => "val2", "key3" => "val3"}// clear() 清除所有
    demo_map.clear()
    demo_map  // Map {} 
    
  • 可以使用任何JavaScript数据类型作为键,使用严格对象相等的标准来检查键的匹配性。

    [注]  Map内部使用SameValueZero比较操作(ECMAScript规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。

    const demo_map = new Map()
    ​
    const functionKey = function() {};
    const symbolKey = Symbol();
    const objectKey = new Object();
    ​
    demo_map.set(functionKey, "functionValue");
    demo_map.set(symbolKey  , "symbolValue");
    demo_map.set(objectKey  , "objectValue");
    ​
    demo_map.get( functionKey ) // "functionValue"
    demo_map.get( symbolKey )   // "symbolValue"
    demo_map.get( objectKey )   // "objectValue"// 使用严格对象相等的标准来检查键的匹配性
    demo_map.get( function() {} ) // undefined// 引用值作为键时,修改其属性仍可访问到指定值
    objectKey.selfKey = 'selfVal'
    demo_map.get( objectKey )   // "objectValue"
    

顺序与迭代

[注]  会维护值插入时的顺序

  • entries()(或者Symbol.iterator属性,它引用entries()

    const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] )
    demo_map.entries === demo_map[Symbol.iterator]  // true
    ​
    demo_map.entries()  // MapIterator {'key1' => 'val1', 'key2' => 'val2'}
    
  • entries() 配合使用 for of 实现遍历

    const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] )
    ​
    for ( let pair of demo_map.entries() ) { console.log(pair) }
    // ['key1', 'val1']
    // ['key2', 'val2']
    
  • 扩展操作[...] 实现MapArray

    const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] )
    ​
    [...demo_map] // [['key1', 'val1'], ['key2', 'val2']]
    
  • forEach(callback, opt_thisArg) 进行遍历

    [注]  传入的回调接收可选的第二个参数,这个参数用于重写回调内部this的值

    const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] )
    ​
    demo_map.forEach(
      (val, key) => console.log(`${key} -> ${val}`)
    )
    // key1 -> val1
    // key2 -> val2
    
  • keys()values()分别返回以插入顺序生成键和值的迭代器:

    const demo_map = new Map( [ ["key1", "val1"], ["key2", "val2"] ] )
    ​
    demo_map.keys()   // MapIterator {'key1', 'key2'}
    demo_map.values() // MapIterator {'val1', 'val2'}for ( let key of demo_map.keys() ) { console.log(key) }
    // key1
    // key2for ( let value of demo_map.values() ) { console.log(value) }
    // val1
    // val2
    

WeakMap

键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError

WeakMapMap的“兄弟”类型,其API也是Map的子集。

WeakMap中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱映射”中键的方式。

基本API

  • set()添加键/值对
  • get()has()查询
  • delete()删除
  • clear()清除 ,跟Map不一样,WeakMap没有clear()

特性

  • 键只能是Object或者继承自Object的类型

    const objKey = { id: 1 }
    const demo_weakMap = new WeakMap( [ [objKey, "val1"] ] )
    demo_weakMap // WeakMap {{…} => 'val1'}/* 原始值不可以直接当 WeakMap 的键 */
    const demo_weakMap = new WeakMap( [ ["key1", "val1"] ] )
    // Uncaught TypeError: Invalid value used as weak map key
    

    [注]  书中表示 原始值可以先包装成对象再用作键, 但是经过测试,Chrome 和 Safari 浏览器中控制台均报错

  • 键的引用要是不在了, weakMap就会被当作垃圾回收

    const demo_weakMap = new WeakMap()
    demo_weakMap.set({}, 'val') // WeakMap {{…} => 'val'}/* 再次打印 demo_weakMap,经多次测试,非立即必现 */
    demo_weakMap  // WeakMap {}
    

image-20220814215436624.png

不可迭代键

因为WeakMap中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力

使用弱映射

  • 私有变量

    • 可能遇到的问题,外部代码只需要拿到对象实例的引用和弱映射,就可以取得“私有”变量了。

      const demo_weakMap = new WeakMap();
      ​
      // 设计初衷,将 id 私有化,无法通过除 user.getId() 之外的途径获取到
      class User {
        constructor(id) {
          this.idProperty = Symbol('id');
          this.setId(id);
        }
      ​
        setPrivate(property, value) {
          const privateMembers = demo_weakMap.get(this) || {};
          privateMembers[property] = value;
          demo_weakMap.set(this, privateMembers);
        }
      ​
        getPrivate(property) {
          return demo_weakMap.get(this)[property];
        }
      ​
        setId(id) {
          this.setPrivate(this.idProperty, id);
        }
      ​
        getId() {
          return this.getPrivate(this.idProperty);
        }
      }
      ​
      const user = new User(123);
      user.getId(); // 123
      user.setId(456);
      user.getId(); // 456// 并不是真正私有的
      demo_weakMap.get(user)[user.idProperty]; // 456
      
    • 用一个闭包把WeakMap包装起来,这样就可以把弱映射与外界完全隔离开了

      // 设计初衷,将 id 私有化,无法通过 user.id 直接获取到
      const User = (() => {
        const demo_weakMap = new WeakMap();
      ​
        class User {
          constructor(id) {
            this.idProperty = Symbol('id');
            this.setId(id);
          }
      ​
          setPrivate(property, value) {
            const privateMembers = demo_weakMap.get(this) || {};
            privateMembers[property] = value;
            demo_weakMap.set(this, privateMembers);
          }
      ​
          getPrivate(property) {
            return demo_weakMap.get(this)[property];
          }
      ​
          setId(id) {
            this.setPrivate(this.idProperty, id);
          }
      ​
          getId(id) {
            return this.getPrivate(this.idProperty);
          }
        }
        return User;
      })();
      ​
      const user = new User(123);
      user.getId(); // 123
      user.setId(456);
      user.getId(); // 456
      
  • DOM节点元数据

    • 相较于Map()

      const demo_map = new Map();
      const loginButton = document.querySelector('#login');
      ​
      // 给这个节点关联一些元数据
      demo_map.set(loginButton, {disabled: true});
      ​
      // 假设在上面的代码执行后,页面被JavaScript改变了,原来的登录按钮从DOM树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的DOM节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。
      
    • WeakMap()的垃圾回收机制优势就体现出来了

      const demo_weakMap = new Map();
      const loginButton = document.querySelector('#login');
      ​
      // 给这个节点关联一些元数据
      demo_weakMap.set(loginButton, {disabled: true});
      ​
      // (假设没有其他地方引用这个对象)
      // 那么当节点从DOM树中被删除后,垃圾回收程序就可以立即释放其内存
      

Set

Set在很多方面都像是加强的Map,这是因为它们的大多数API和行为都是共有的。

基本API

  • 使用嵌套数组初始化

    const s1 = new Set(["val1", "val2", "val3"]);
    s1  // Set(3) {'val1', 'val2', 'val3'}
    
  • 使用自定义迭代器初始化

    const s2 = new Set({
      [Symbol.iterator]: function*() {
        yield "val1";
        yield "val2";
        yield "val3";
      }
    });
    s2  // Set(3) {'val1', 'val2', 'val3'}
    
  • 使用add()方法添加元素

    const demo_set = new Set()
    demo_set.add("val1")
            .add("val2")
    demo_set  // Set(2) {'val1', 'val2'}// add() 同样的值会默认略过
    demo_set.add("val1")
    demo_set  // Set(2) {'val1', 'val2'}
    
  • 使用has()进行查询,通过size属性获取长度

    const demo_set = new Set(["val1", "val2"]);
    demo_set.has("val1")  // true
    demo_set.size         // 2
    
  • 使用delete()clear()删除值

    const demo_set = new Set(["val1", "val2", "val3"]);
    demo_set  // Set(3) {'val1', 'val2', 'val3'}// delete() 删除指定项
    // 返回 Set 中是否存在被删除的值
    demo_set.delete('val1') // true
    demo_set.delete('val1') // false
    demo_set  // Set(2) {'val2', 'val3'}// clear() 清除所有
    demo_set.clear()
    demo_set  // Set(0) {size: 0}
    
  • 可以使用任何JavaScript数据类型作为值,使用严格对象相等的标准来检查键的匹配性。

    [注]  Set内部使用SameValueZero比较操作(ECMAScript规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。

    const demo_set = new Set()
    ​
    const functionVal = function() {};
    const symbolVal = Symbol();
    const objectVal = new Object();
    ​
    demo_set.add( functionVal );
    demo_set.add( symbolVal );
    demo_set.add( objectVal );
    ​
    demo_set.has( functionVal ) // true
    demo_set.has( symbolVal )   // true
    demo_set.has( objectVal )   // true// 使用严格对象相等的标准来检查键的匹配性
    demo_set.has( function() {} ) // false// 引用值作为键时,修改其属性仍可访问到指定值
    objectVal.selfKey = 'selfVal'
    demo_set.has( objectVal )   // true
    

顺序与迭代

[注]  会维护值插入时的顺序

  • values() 及其别名方法keys()(或者Symbol.iterator属性,它引用values()

    const demo_set = new Set(["val1", "val2"]);
    demo_set.values === demo_set[Symbol.iterator] // true
    demo_set.keys   === demo_set[Symbol.iterator] // true
    ​
    demo_set.values() // SetIterator {'val1', 'val2'}
    
  • values() 配合使用 for of 实现遍历

    const demo_set = new Set(["val1", "val2"]);
    ​
    for ( let value of demo_set.values() ) { console.log(value) }
    // val1
    // val2
    
  • 扩展操作[...] 实现SetArray

    const demo_set = new Set(["val1", "val2"]);
    ​
    [...demo_set] // ["val1", "val2"]
    
  • entries()方法返回一个迭代器,可以按照插入顺序产生包含两个元素的数组,这两个元素是集合中每个值的重复出现

    const demo_set = new Set(["val1", "val2"]);
    ​
    for ( let pair of demo_set.entries() ) { console.log(pair) }
    // ['val1', 'val1']
    // ['val2', 'val2']
    
  • forEach(callback, opt_thisArg) 进行遍历

    [注]  传入的回调接收可选的第二个参数,这个参数用于重写回调内部this的值

    const demo_set = new Set(["val1", "val2"]);
    ​
    demo_set.forEach(
      (val, dupVal) => console.log(`${val} -> ${dupVal}`)
    )
    // val1 -> val1
    // val2 -> val2
    

特定操作

  • 返回两个或更多集合的并集

    class XSet extends Set {
      union(...sets) {
        return XSet.union(this, ...sets)
      }
    ​
      // 返回两个或更多集合的并集
      static union(a, ...bSets) {
        const unionSet = new XSet(a);
        for (const b of bSets) {
          for (const bValue of b) {
            unionSet.add(bValue);
          }
        }
        return unionSet;
      }
    }
    ​
    const _xSet = new XSet()
    _xSet.union(
      new Set([1, 2]), 
      new Set([3, 4]),
      new Set([5, 6])
    ) // XSet(6) {1, 2, 3, 4, 5, 6}
    
  • 返回两个或更多集合的交集

    class XSet extends Set {
      union(...sets) {
        return XSet.union(this, ...sets)
      }
      intersection(...sets) {
        return XSet.intersection(this, ...sets);
      }
      
      // 返回两个或更多集合的并集
      static union(a, ...bSets) {
        const unionSet = new XSet(a);
        for (const b of bSets) {
          for (const bValue of b) {
            unionSet.add(bValue);
          }
        }
        return unionSet;
      }
      /**
       * 原著中 intersection 函数有误(也可能是我没get到他的函数使用方法),并不能返回交集
       * 经过修改后,支持返回集合交集。
       */
      // 返回两个或更多集合的交集
      static intersection(a, ...bSets) {
        // const intersectionSet = new XSet(a)    
        const intersectionSet = new XSet(a).union( ...bSets );
        for (const aValue of intersectionSet) {
          for (const b of bSets) {
            if (!b.has(aValue)) {
              intersectionSet.delete(aValue);
            }
          }
        }
        return intersectionSet;
      }
    }
    ​
    const _xSet = new XSet()
    _xSet.intersection(
      new Set([1, 2, 3]), 
      new Set([2, 3, 4]),
      new Set([2, 3, 6])
    ) // XSet(2) {2, 3}
    

WeakSet

WeakSetSet的“兄弟”类型,其API也是Set的子集。WeakSet中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱集合”中值的方式。

基本API

  • add()添加值
  • has()查询
  • delete()删除
  • clear()清除 ,跟Set不一样,WeakSet没有clear()

特性

  • 值只能是Object或者继承自Object的类型

    const objVal = { id: 1 }
    const demo_weakSet = new WeakSet( [ objVal ] )
    demo_weakSet // WeakSet {{…}}/* 原始值不可以直接当 WeakSet 的键 */
    const demo_weakSet = new WeakSet( [ 'val' ] )
    // Uncaught TypeError: Invalid value used in weak set
    

    [注]  书中表示 原始值可以先包装成对象再用作键, 但是经过测试,Chrome 和 Safari 浏览器中控制台均报错

  • 键的引用要是不在了, WeakSet就会被当作垃圾回收

    const demo_weakSet = new WeakSet()
    demo_weakSet.add({}) // WeakSet {{…}}/* 再次打印 demo_weakSet,经多次测试,非立即必现*/
    demo_weakSet  // WeakSet {}
    

不可迭代键

因为WeakSet中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力

使用弱集合

相比于WeakMap实例,WeakSet实例的用处没有那么大。不过,弱集合在给对象打标签时还是有价值的。

  • 相较于Set

    const disabledElements = new Set();
    ​
    const loginButton = document.querySelector('#login');
    ​
    // 通过加入对应集合,给这个节点打上“禁用”标签
    disabledElements.add(loginButton);
    ​
    // 这样,通过查询元素在不在disabledElements中,就可以知道它是不是被禁用了。
    // 不过,假如元素从DOM树中被删除了,它的引用却仍然保存在Set中,因此垃圾回收程序也不能回收它。
    
  • WeakSet()的垃圾回收机制优势就体现出来了

    const disabledElements = new WeakSet();
    ​
    const loginButton = document.querySelector('#login');
    ​
    // 通过加入对应集合,给这个节点打上“禁用”标签
    disabledElements.add(loginButton);
    ​
    // (假设没有其他地方引用这个对象)
    // 这样,只要WeakSet中任何元素从DOM树中被删除,垃圾回收程序就可以忽略其存在,而立即释放其内存