ES6常用但被忽略的方法(第三弹Symbol、Set 和 Map )

2,285 阅读8分钟

写在开头

  • 明天就是端午节了,提前祝大家端午安康。
  • ES6常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。

相关文章

Symbol

特性

  1. 唯一性
  • 属性名属于 Symbol 类型的,都是独一无二的,可以保证不会与其他属性名产生冲突。即使是两个声明完全一样的也是不相等的。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
  1. 不能和其他类型运算
  • Symbol 值不能与其他类型的值进行运算,会报错。
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
  1. 类型转换
  • Symbol 值可以显式转为字符串和布尔值,但是不能转为数值。
let sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'

let sym = Symbol();
Boolean(sym) // true
!sym  // false

Number(sym) // TypeError
sym + 2 // TypeError

Symbol的应用

  1. 常见的唯一值
  • Symbol本身的特性就是任何两个Symbol类型的值都不相等,所以我们可以在不知到命名变量时,都设置为Symbol类型。Symbol类型不能使用new操作符,因为生成的 Symbol 是一个原始类型的值,不是对象。
  1. 私有属性
  • 由于Symbol类型的作为属性名时,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。需要通过Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。这样就可以把Symbol类型的属性作为私有属性。
  • 新的 APIReflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
  1. 消除魔法字符串
  • 魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。我们可以把对应的字符串或数值设置成 Symbol 类型即可。
const name = {
    first: Symbol('detanx')
}
function getName(firstName) {
  switch (firstName) {
    case name.first: // 魔术字符串
      ...
  }
}

getName(name.first); // 魔术字符串

Symbol.for()和Symbol.keyFor()

  • Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。即如果传入相同的key创建的值是相等的。而Symbol()每次创建的值都不相同,即使key相同。
let s1 = Symbol.for('detanx');
let s2 = Symbol.for('detanx');
s1 === s2 // true

let s1 = Symbol('detanx');
let s2 = Symbol('detanx');
s1 === s2 // false
  • Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key。未登记的 Symbol 值,返回undefined
let s1 = Symbol.for("detanx");
Symbol.keyFor(s1) // "detanx"

let s2 = Symbol("detanx");
Symbol.keyFor(s2) // undefined
  • Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。
  • Symbol.for()Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。

内置的Symbol值

Set 和 Map 数据结构

Set

  1. 特性
    • 类似于数组,但是成员的值都是唯一的,没有重复的值。
    • Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
    • 内部两个数据是否相同基本和===类似,区别是在SetNaNNaN也是相等的。
  2. 应用
    • 数组或字符串去重。
    [...new Set([1, 1, 2, 3])] // [1, 2, 3]
    
    [...new Set('ababbc')].join('') // "abc"
    
    • 某些需要唯一性的数据,例如:用户名,id 等。
  3. Set 实例的属性和方法
    • Set 结构的实例有以下属性。
    Set.prototype.constructor:构造函数,默认就是 Set 函数。
    Set.prototype.size:返回Set实例的成员总数。
    
    • 操作方法(用于操作数据)
    Set.prototype.add(value):添加某个值,返回 Set 结构本身。
    Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    Set.prototype.has(value):返回一个布尔值,表示该值是否为 Set 的成员。
    Set.prototype.clear():清除所有成员,没有返回值。
    
    • 遍历方法(用于遍历成员)
    Set.prototype.keys():返回键名的遍历器
    Set.prototype.values():返回键值的遍历器
    Set.prototype.entries():返回键值对的遍历器
    Set.prototype.forEach():使用回调函数遍历每个成员
    

WeakSet

  1. Set的区别
  • WeakSet 的成员只能是对象(null除外),而不能是其他类型的值。
const ws = new WeakSet();
ws.add(1) // TypeError: Invalid value used in weak set
ws.add(Symbol())  // TypeError: invalid value used in weak set
ws.add(null)  // TypeError: invalid value used in weak set
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet之中。
  1. 方法。
WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
  • WeakSet 没有size属性,没有办法遍历它的成员。

Map

  • 由于传统的Object对象只能使用字符串当作键,所以新增的Map结构可以将各种类型的值(包括对象)都可以当作键。
const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"
  1. 方法
  • 相比Set的操作方法,Map没有add方法,新增了get方法和set方法。遍历方法则是基本是一样的。
Map.prototype.get(key) // 读取key对应的键值,如果找不到key,返回undefined。
Map.prototype.has(key) // 返回一个布尔值,表示某个键是否在当前 Map 对象之中。
  1. 转换
    (1) Map和数组
    • 在第二弹中也提到了Map和数组之间转换,他们之间互转是很方便的。
    // Map转为数组
    const myMap = new Map()
      .set(true, 7)
      .set({foo: 3}, ['abc']);
    [...myMap]
    // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
    
    // 数组转为 Map。
    new Map([
      [true, 7],
      [{foo: 3}, ['abc']]
    ])
    // Map {
    //   true => 7,
    //   Object {foo: 3} => ['abc']
    // }
    
    (2) Map和对象
    • Map 的键都是字符串,它可以无损地转为对象。如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。对象转为 Map 可以通过Object.entries()
    // Map => Object
    function strMapToObj(strMap) {
      let obj = Object.create(null);
      for (let [k,v] of strMap) {
        obj[k] = v;
      }
      return obj;
    }
    
    const myMap = new Map()
      .set('yes', true)
      .set('no', false);
    strMapToObj(myMap)
    // { yes: true, no: false }
    
    // Object => Map
    // let obj = {"a":1, "b":2};
    let map = new Map(Object.entries(obj));
    
  2. 应用
  • 在存储的数据是键值对的时候,并且键名的类型可能是多种类型是可以使用Map结构。java中的Map结构有区别。

WeakMap

  1. Map的区别
    • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
    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
    map.set(new Number(1), 2) // WeakMap {Number => 2}
    
    • WeakMap的键名所指向的对象,不计入垃圾回收机制。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放占用的内存。
    const e1 = document.getElementById('foo');
    const e2 = document.getElementById('bar');
    const arr = [
      [e1, 'foo 元素'],
      [e2, 'bar 元素'],
    ];
    // 不需要 e1 和 e2 的时候
    // 必须手动删除引用
    arr [0] = null;
    arr [1] = null;
    
    • WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
    • 没有遍历操作(即没有keys()values()entries()方法),也没有size属性。
    • 无法清空,即不支持clear方法。WeakMap只有四个方法可用:get()set()has()delete()
  2. 应用
    (1) DOM 节点作为键名
    • document.getElementById('logo')是一个DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
    let myWeakmap = new WeakMap();
    
    myWeakmap.set(
      document.getElementById('logo'),
      {timesClicked: 0})
    ;
    
    document.getElementById('logo').addEventListener('click', function() {
      let logoData = myWeakmap.get(document.getElementById('logo'));
      logoData.timesClicked++;
    }, false);
    
    (2) 部署私有属性
    • 内部属性是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。
    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.dec()
    // DONE