ES6:symbol类型

418 阅读6分钟

一、symbol是什么

symbol是ES6出的一种基本数据类型,通过Symbol函数返回symbol类型的值,并且每个从Symbol函数返回的symbol值是唯一的

如何确定数据类型是不是symbol:

      console.log(typeof Symbol()) // symbol
      console.log(Object.prototype.toString.call(Symbol())) // [object Symbol]

二、声明symbol的方式

1、第一种方式:直接调用Symbol函数创建唯一的symbol

语法:

Symbol(description) // description是可选的字符串,仅用于调试,传入的数据会被隐式转换为string

如果description是一个对象,会调用对象中的Symbol.toPrimitive(将对象转换为基本数据类型)方法,如果没有就会调用toString方法,如果这两个方法都没有,那么description就是一个object类型:

      const obj = {
        [Symbol.toPrimitive]() {
          return 'Symbol.toPrimitive'
        },
        toString() {
          return 'toString'
        }
      }

      const symbol = Symbol(obj)
      const symbol1 = Symbol({})
      console.log(symbol.description) // Symbol.toPrimitive
      console.log(symbol1.description) // [object Object]

Symbol的创建类似于内置的对象类,但是它不是构造函数,不支持new Symbol(),直接调用Symbol函数便可以得到一个symbol。Symbol函数中的参数是一个描述,可以通过toString/description得到这个描述:

      const symbol1 = Symbol()
      const symbol2 = Symbol(42)
      const symbol3 = Symbol('foo')

      console.log(symbol1, symbol1.toString(), symbol1.description) // Symbol() 'Symbol()' undefined
      console.log(symbol2, symbol2.toString(), symbol2.description) // Symbol(42) 'Symbol(42)' '42'
      console.log(symbol3, symbol3.toString(), symbol3.description) // Symbol(foo) 'Symbol(foo)' 'foo'

需要知道的是,每次调用Symbol函数都会生成一个新的symbol:

      console.log(Symbol() === Symbol()) // false
      console.log(Symbol('f') === Symbol('f')) // false

2、第二种方式:使用Symbol.for()定义全局的symbol

当你使用Symbol.for()去定义symbol,此时的symbol已经被保存为全局的,当再次使用相同的description,在内存中访问的是同一个symbol:

      const symbol1 = Symbol.for('xx描述')
      let symbol2
      const fn = () => {
        symbol2 = Symbol.for('xx描述')
      }
      fn()

      console.log(symbol1 === symbol2) // true

对于Symbol.for()定义的symbol,通过Symbol.keyfor()获取到该全局symbol的描述:

      console.log(Symbol.keyFor(Symbol()), typeof Symbol.keyFor(Symbol())) // undefined 类型是undefined
      console.log(Symbol.keyFor(Symbol.for())) // 'undefined' 类型是string
      console.log(Symbol.keyFor(Symbol.for(12))) // '12' 类型是string

三、Symbol的属性和方法

1、Symbol.iterator,被for...of使用

Symbol.iterator是for...of循环的底层机制,可以用Symbol.iterator来判断一个数据类型是否可以被for...of遍历到:

      console.log([][Symbol.iterator]) // ƒ values() { [native code] }
      console.log(new Set()[Symbol.iterator]) // ƒ values() { [native code] }
      console.log(new Map()[Symbol.iterator]) // ƒ entries() { [native code] }
      console.log(''[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] }
      const labels = document.querySelectorAll('*') // 类数组
      console.log(labels[Symbol.iterator]) // ƒ values() { [native code] }
      console.log({}[Symbol.iterator]) // undefined
      console.log(new Number()[Symbol.iterator]) // undefined
      console.log(new Boolean()[Symbol.iterator]) // undefined

数组/Set/Map/类数组/字符串可以被for...of遍历,对象/数字/布尔值不可以被for...of遍历。for...of循环就是调用该数据结构的[Symbol.iteartor]函数,这个函数安装Iteartor迭代器规范设计的,手动实现迭代器:

Iteartor迭代器规范:

  1. 每一次循环都执行一次next函数
  2. next函数返回一个对象,对象中两个属性,一个是done表示循环是否结束,一个是value表示值
      const arr = ['a', 'b', 'c']
      
      arr[Symbol.iterator] = function () {
        var _this = this
        var index = 0
        return {
          next: function () {
            if (index > _this.length - 1)
              return { done: true, value: undefined }
            return { done: false, value: _this[index++] }
          }
        }
      }

      for (const item of arr) {
        console.log(item)
      }

2、Symbol.asyncIterator,被for await of使用

for await of在执行时实际上是调用该数据的[Symbol.asyncIterator]函数,底层使用生成器函数实现的

      const fn1 = () =>
        new Promise((res) => {
          setTimeout(() => {
            res(1)
          }, 1000)
        })

      const fn2 = () =>
        new Promise((res) => {
          res(2)
        })

      const fn3 = () =>
        new Promise((res) => {
          setTimeout(() => {
            res(3)
          }, 1000)
        })

      const test = async () => {
        const obj = {}
        obj[Symbol.asyncIterator] = async function* () {
          yield fn1()
          yield fn2()
          yield fn3()
        }
        for await (const item of obj) {
          console.log(item)
        }
        /*
          依次打印1 2 3
        */
      }
      test()

3、Symbol.hasInstance,被instanceof使用

当使用instanceof时,底层调用的是类的静态方法[Symbol.hasInstance]

      class F {
        constructor() {
          this.x = Symbol.for('x')
        }
      }

      const f = new F()
      console.log(f instanceof F) // true 毋庸置疑

      const arr = ['a', 'b', 'c']
      console.log(arr instanceof F) // false 毋庸置疑
      Object.setPrototypeOf(arr, F.prototype) // 相当于arr.__proto__ = F.prototype
      console.log(arr instanceof F) // true 因为已经将F的原型赋值给arr的__proto__了

如果不希望强行改变__proto__的指向来影响instanceof的结果,可以改写[Symbol.hasInstance]方法:

        static [Symbol.hasInstance](obj) {
          return obj.x && obj.x === Symbol.for('x')
        }

4、Symbol.toPrimitive,将对象转换为基本数据类型

当使用==来比较对象和原始值时,首先会调用对象中的[Symbol.toPrimitive]方法,参考对象和原始值的比较

5、Symbol.toStringTag,被Object.prototype.toString()使用

当创建自己的类时,js默认采用Object标签:

      function F() {}
      const f = new F()
      console.log(Object.prototype.toString.call(f)) // [object, Object]

可以通过[Symbol.toStringTag]设置自己的自定义标签:

      F.prototype[Symbol.toStringTag] = 'F'
      console.log(Object.prototype.toString.call(f)) // [object, F]

四、对象中symbol属性的迭代

1、for...in不会遍历到symbol类型的属性

      const password = Symbol('password')
      const obj = {
        name: 'xx',
        age: 18,
        [password]: '123456'
      }

      for (const key in obj) {
        console.log(key) // 只会打印2个键:name age
      }

2、Object.key()仅获取对象自身非symbol类型的私有属性

      const arr = Object.keys(obj)
      console.log(arr) // ['name', 'age']

3、Object.getOwnPropertyNames()也无法获取symbol类型的属性

      const names = Object.getOwnPropertyNames(obj)
      console.log(names) // ['name', 'age']

4、JSON.stringify会自动过滤掉symbol类型的属性

      console.log(JSON.stringify(obj)) // {"name":"xx","age":18} 当把一些数据存到localStorage中时,密码属于隐私数据,便可以采用symbol作为密码的键

5、Object.getOwnPropertySymbols()仅获取到对象中symbol类型的私有属性

      const symbolList = Object.getOwnPropertySymbols(obj)
      console.log(symbolList) // [Symbol(password)]

6、Reflect.ownKeys()获取对象中所有的私有属性

      const all = Reflect.ownKeys(obj)
      console.log(all) // ['name', 'age', Symbol(password)]

五、symbol的使用场景

1、使用symbol代替常量

(1)使用symbol代替id

比如说一个班级中所有的学生的分数,收集到一个对象中,假如有两个叫“张三”的同学,那么后一个会覆盖前一个:

      const students = { 张三: 90, 张三: 100 }

      console.log(students['张三']) // 100

我们可以变一下,通过id去存取分数:

      const s1 = { name: '张三', id: 1 }
      const s2 = { name: '张三', id: 2 }

      const students = { [s1.id]: 90, [s2.id]: 100 }

      console.log(students[s1.id]) // 90
      console.log(students[s2.id]) // 100

以上id具体的值就可以使用symbol(Symbol函数会生成一个永不重复的值):

      const s1 = { name: '张三', id: Symbol() }
      const s2 = { name: '张三', id: Symbol() }

      const students = { [s1.id]: 90, [s2.id]: 100 }

      console.log(students[s1.id]) // 90
      console.log(students[s2.id]) // 100

在这里id写成数字和写成Symbol()的效果是一样的,那好像用symbol麻烦一点呢?假如说数据量比较大,写100个呢,万一手滑了把两个id写成一样的,那不就gg了,但是每次调用Symbol()返回的值都是唯一的。

由于symbol的唯一性,所以symbol可以用来当做标识符使用,当使用symbol作为对象的键,那么对象中的键一定是唯一的。当我们想操作一个由多个模块构成的对象,向这个对象中添加某个属性时,可以用symbol作为键,避免对象中某个属性被修改或覆盖

      const name = Symbol('name')
      const obj = {...} // 这是一个非常复杂的对象
      obj[name] = 'xxx' // 追加一个属性,使用symbol作为键一定不会对原属性造成冲突

在对象中,使用symbol存取值时,symbol值必须要放到方括号中,实例可以看下call方法的手动实现

(2)消除魔法字符串:使用symbol代替某个固定的字符串常量

啥叫魔法字符串?

      const getBool = (val) => {
        return val === 'name' ? true : false
      }
      getBool('name')

以上代码中的'name'就是魔法字符串,'name'如果使用多了,要更改这样的字符串就会比较麻烦,通常可以定义成一个常量:

      const str = 'name'
      const getBool = (val) => {
        return val === str ? true : false
      }
      getBool(str)

你会发现这个常量是什么值并不重要,所以可以使用symbol:

      const str = Symbol()
      const getBool = (val) => {
        return val === str ? true : false
      }
      getBool(str)

(3)vuex/redux中action-type.js中定义的常量

      export const add = 'ADD'
      export const minus = 'MINUS'

      dispatch({
        type: add,
        ...
      })

类似这样的代码,就可以使用symbol代替字符串常量

      export const add = Symbol()
      export const minus = Symbol()

2、保护个别隐私属性

在对象中,如果键是symbol,使用for...in/Object.keys/Object.getOwnPropertyNames/JSON.stringify是获取不到的,对于一些不希望被别人通过常规方式遍历到的属性可以考虑使用symbol去定义