• ES6 Symbol


    在这里插入图片描述

    前言

      此文对ES6中涉及的Symbol类型做了简单说明,也包括部分开放的内置Symbol

    属性方法

      Symbol 为符号类型,属于基本数据类型之一。

    基本数据类型 也称为原始数据类型,包括StringNumberBooleanundefinednullSymbolBigInt,其中SymbolBigIntES6新增

      Symbol()可以用来生成唯一值,也是ES6引入Symbol的原因。

    Symbol() === Symbol() // false
    
    • 1

      创建一个Symbol包装对象。

    var sym = Symbol()
    var object = Object(sym) // Symbol {Symbol(), description: undefined}
    
    typeof sym // symbol
    typeof object // object
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Symbol.prototype.description

      Symbol.prototype.description 用于返回Symbol的描述信息,StringtoString方法会包含Symbol()字符串。

    var sym = Symbol('desc')
    
    sym.description // desc
    sym.toString() // Symbol(desc)
    String(sym) // Symbol(desc)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Symbol.for

      与Symbol()不同的是,Symbol.for 除了会创建Symbol符号之外,还会把它放入全局的Symbol注册表。

    注册表可以想象为一个对象,键keySymbol的描述信息,键值为Symbol符号

      Symbol.for()并非每次都会创建一个新的Symbol,而是检查指定key是否已经在注册表中,若在则返回已保存的Symbol,否则就新建一个并放入全局注册表。

    Symbol.for('desc') === Symbol.for('desc') // true
    
    • 1

    Symbol.keyFor

      Symbol.keyFor 用于获取指定的Symbol符号,存储在全局注册表里对应的key键。

    var s = Symbol(),
      y = Symbol.for(),
      m = Symbol.for('desc')
    
    Symbol.keyFor(s) // undefined
    Symbol.keyFor(y) // 'undefined'
    Symbol.keyFor(m) // 'desc'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意sy符号,分别返回undefined和字符串'undefined'

      可封装工具函数,判断是否位于全局注册表中。

    function inGlobalRegistry(sym) {
      return !!Symbol.keyFor(sym)
    }
    
    • 1
    • 2
    • 3

    Symbol

      ES6开放了一部分内置的Symbol符号,注意规范中内置符号前缀为@@,例如@@hasInstance表示Symbol.hasInstance

    Symbol.hasInstance

      instanceof 用于检测构造函数的原型是否在实例对象的原型链上。

    function F() { }
    var f = new F()
    
    f instanceof F // true
    f instanceof Object // true
    
    • 1
    • 2
    • 3
    • 4
    • 5

      函数实现。

    function instanceOf(object, constructor) {
      // or object.__proto__
      while ((object = Object.getPrototypeOf(object))) {
        if (object === constructor.prototype) {
          return true
        }
      }
    
      return false
    }
    
    instanceOf(String, Object) // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

      instanceof在语言内部将执行 Symbol.hasInstance,例如f instanceof F即执行的是F[Symbol.hasInstance](f)

    function F() { }
    var f = new F()
    
    f instanceof F // true
    F[Symbol.hasInstance](f) // true
    
    • 1
    • 2
    • 3
    • 4
    • 5

      自定义instanceof

    var F = {
      [Symbol.hasInstance](v) {
        return v % 2 === 0
      },
    }
    
    1 instanceof F // false
    2 instanceof F // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Symbol.isConcatSpreadable

      Symbol.isConcatSpreadable 即数组或类数组在被concat拼接时,控制是否能展开。

      数组Symbol.isConcatSpreadable属性默认为undefined,可以展开。

    var array = [1, 2]
    
    [0].concat(array, 3) // [0, 1, 2, 3]
    
    array[Symbol.isConcatSpreadable] = false
    
    [0].concat(array, 3) // [0, [1, 2], 3]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      而类数组默认不可展开。

    var arrayLike = {
      0: 1,
      1: 2,
      length: 2,
    }
    
    [0].concat(arrayLike, 3) // [0, { 0: 1, 1: 2, length: 2 }, 3]
    
    arrayLike[Symbol.isConcatSpreadable] = true
    
    [0].concat(arrayLike, 3) // [0, 1, 2, 3]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Symbol.species

      ES6extends存在一个有趣的现象,即内置方法返回的对象都将默认成为派生类的实例。

      什么意思呢?

      例如SortedArray继承自Array,而内置方法map返回的数组成为了SortedArray的实例。

    class SortedArray extends Array {}
    
    const sortedArray = new SortedArray(3, 1, 2)
    const array = sortedArray.map(e => e)
    
    array instanceof SortedArray // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

      如何做到的呢?

      以下为Array类内部的大致结构。

    class Array {
      static get [Symbol.species]() {
        return this
      }
      
      ...
    
      map(callback) {
        const Constructor = this.constructor[Symbol.species]()
        const result = new Constructor(this.length)
    
        for (var i = 0; index < result.length; i++) {
          result[i] = callback(i, this[i], this)
        }
    
        return result
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

      运行sortedArray.map()时,map函数内thissortedArray实例,this.constructorSortedArray派生类。另外静态取值方法[Symbol.species]()内部返回调用者,则this.constructor[Symbol.species]()SortedArray类。

      故sortedArray.map()返回的数组也就是由SortedArray类创建的,array instanceof SortedArray也就必然为true了。

      ES6中将Symbol.species开放,子类可以覆盖父类的[Symbol.species]()静态方法。

    class SortedArray extends Array {
      static get [Symbol.species]() {
        return Array
      }
    }
    
    const sortedArray = new SortedArray(3, 1, 2)
    const array = sortedArray.map(e => e)
    
    array instanceof SortedArray // false
    array instanceof Array // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      根据刚才的分析,结合Array的内部结构,容易知道sortedArray.map()返回的数组是由Array类创建的,因此array instanceof SortedArrayfalse

      有何作用呢?

      某些类库可能继承至基类,子类使用基类的方法时,更多的,希望返回的对象是基类的实例,而非子类的实例。例如以上SortedArray继承至基类Array,子类实例sortedArray使用map方法时,希望返回的数组是Array的实例。

      所以 Symbol.species 作用为,子类继承基类,子类方法返回新对象时,指定新对象的类(或者说构造函数)。

    Symbol.match

      Symbol.matchString.prototype.math在语言内部将执行RegExp.prototype[Symbol.match]

    var regexp = /llo/, s = 'hello'
    
    s.match(regexp) // ['llo', index: 2, input: 'hello', groups: undefined]
    regexp[Symbol.match](s) // ['llo', index: 2, input: 'hello', groups: undefined]
    
    • 1
    • 2
    • 3
    • 4

    Symbol.replace

      Symbol.replaceString.prototype.replace在语言内部将执行RegExp.prototype[Symbol.replace]

    var regexp = /llo/, s = 'hello'
    
    s.replace(regexp, 'he') // hehe
    regexp[Symbol.replace](s, 'he') // hehe
    
    • 1
    • 2
    • 3
    • 4

    Symbol.search

      Symbol.searchString.prototype.search在语言内部将执行RegExp.prototype[Symbol.search]

    var regexp = /llo/, s = 'hello'
    
    s.search(regexp) // 2
    regexp[Symbol.search](s) // 2
    
    • 1
    • 2
    • 3
    • 4

    Symbol.split

      Symbol.splitString.prototype.split在语言内部将执行RegExp.prototype[Symbol.split]

    var regexp = new RegExp(''), s = 'hello'
    
    s.split(regexp, 3) // ['h', 'e', 'l']
    regexp[Symbol.split](s, 3) // ['h', 'e', 'l']
    
    • 1
    • 2
    • 3
    • 4

    String.prototype.split()方法中第一个参数为字符串或者正则表达式,第二个参数用于限制分割后的数组长度。

    Symbol.iterator

      Symbol.iterator 为对象部署迭代器,可被用于for...of循环、拓展运算符和解构等。

    const object = { foo: 1, bar: 2 }
    
    object[Symbol.iterator] = function () {
      const keys = Object.keys(this)
      var index = 0
    
      return {
        next() {
          return {
            done: index === keys.length,
            value: keys[index++],
          }
        },
      }
    }
    
    for (const key of object) {
      console.log(key)
      // foo
      // bar
    }
    
    [...object] // ['foo', 'bar']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Symbol.toPrimitive

      对象转换为原始值时,在JavaScript内部会进行 ToPrimitive 抽象运算。

      例如将对象转换为字符串类型。

    String({ foo: 1 }) // [object Object]
    
    • 1

      ToPrimitive抽象运算可以想象为一个ToPrimitive(input, preferredType)方法,input为被转换对象,preferredType为期望返回的结果类型。

      preferredType包括numberstringdefault三种,不同场景下preferredType值不同。

    • number+object正运算、Number(object)
    • string${object}模板字符串插值、foo[object]对象用作属性、string.search(object)String(object)parseInt(object)
    • defaultobject + x加法运算、object == x相等判断等

    例如Number(object)运算场景下,preferredTypenumber

    ToPrimitive

      ToPrimitive(input, preferredType)运算过程简述为。

    • 判断input是否为非对象(原始值),是则返回input
    • 否则,判断对象是否有[Symbol.toPrimitive](hint){}方法,若有
      • hint参数值初始化为preferredType。注意若preferredType不存在,hint默认为default
      • 若方法的执行结果为非对象(原始值),则返回,否则抛出TypeError错误
    • 否则,执行OrdinaryToPrimitive(input, preferredType)
    const foo = {
      [Symbol.toPrimitive](hint) {
        return '1.00'
      },
    }
    const bar = {
      [Symbol.toPrimitive](hint) {
        return {}
      },
    }
    
    Number(foo) // 1
    Number(bar) // Uncaught TypeError: Cannot convert object to primitive value at Number
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    OrdinaryToPrimitive

      OrdinaryToPrimitive(input, hint)运算过程简述为。

    • hintstring,先调用toString(),如果为非对象(原始值)那么返回它。否则再调用valueOf(),如果为非对象(原始值)那么返回它,否则抛出TypeError错误
    • hintnumber/default,恰好相反,会先调用valueOf(),再调用toString()
    const foo = {
      valueOf() {
        return {}
      },
      toString() {
        return '1.00'
      },
    }
    const bar = {
      valueOf() {
        return {}
      },
      toString() {
        return {}
      },
    }
    
    Number(foo) // 1
    Number(bar) // Uncaught TypeError: Cannot convert object to primitive value at Number
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    用例

      可能你会问,抽象方法很少用到吧。

      并非哦,我们以计算[1, 2] + {}结果为例。

      实际形如 x + y 的表达式,将分别对xy执行ToPrimitive(input, preferredType)抽象运算,转化为原始值。

      参考刚才的场景,容易知道preferredTypedefault,即调用valueOf()toString()

    var x = [1, 2],  y = {}
    
    x.valueOf() // [1, 2]
    x.toString() // '1, 2'
    y.valueOf() // {}
    y.toString() // '[object Object]'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

      所以x + y结果为1, 2[object Object]

    Symbol.toStringTag

      Symbol.toStringTag 用于向Object.prototype.toString提供标签。

    var object = {
      [Symbol.toStringTag]: 'Hello',
    }
    
    object.toString() // [object Hello]
    Object.prototype.toString.call(object) // [object Hello]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    object.toString()Object.prototype.toString.call(object)两者是等价的

    Symbol.unscopables

    with

      以下代码中,console.log将沿着fn函数作用域、全局作用域依次寻找foo

    var foo = 1
    
    function fn() {
      console.log(foo) // 1
    }
    
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      函数fn引入withconsole.log将沿着object对象、fn函数作用域、全局作用域寻找foo

    var foo = 1
    
    function fn() {
      var object = { foo: 2 }
    
      with (object) {
        console.log(foo) // 2
      }
    }
    
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      因此 with 作用非常简单,即扩展了语句的作用域。

      注意若对象上没有某个属性,则将会沿着作用域向上寻找对应变量,若都没有则将抛出错误。

    var foo = 1, bar = 3
    
    function fn() {
      var object = { foo: 2 }
    
      with (object) {
        console.log(bar) // 3
        console.log(baz) // Uncaught ReferenceError: baz is not defined
      }
    }
    
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

      with优势即可以使内部表达式更加简洁,但是语义会不明显,且属性的寻找实际上更加耗时,得不偿失。

    function fn() {
      var object = { foo: 1, bar: 2, baz: 3 }
    
      with (object) {
        // 等价于 object.foo + object.bar + object.baz
        console.log(foo + bar + baz) // 6
      }
    }
    
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

      缺点也很明显,严重时将造成代码歧义。例如以下y可能是x的属性值,也可能是函数的第二个参数y

    function fn(x, y) {
      with (x) {
        console.log(y)
      }
    }
    
    fn({ y: 1 }) // 1
    fn({}, 2) // 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    兼容性

      在框架 extjs 中存在类似如下的代码。

    function fn(values) {
      with (values) {
        console.log(values)
      }
    }
    
    fn([])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      在ES5浏览器中(例如IE10),可能为values.values属性值或者为函数参数values。而values.values属性并不存在,则将沿着作用域寻找到values变量,输出[]

      实际上并没有什么问题,对吧?

      但是在ES6中,数组原型上部署了values方法,values.values将会获取为数组原型的values方法,输出values(){ }

      呐,问题就严重咯~

      代码在行为上与以前不一致了,规范的兼容性被破坏了。

      思考下怎么解决呢?

      能不能在with (object) { }上定义一个规则,让内部不会在对象object上寻找属性呢。

      也就有了 Symbol.unscopables,用于排除with环境中的属性。

    var foo = 1
    
    function fn() {
      var object = {
        foo: 2,
        [Symbol.unscopables]: {
          foo: true,
        },
      }
    
      with (object) {
        console.log(foo) // 1
      }
    }
    
    fn()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

      数组原型上的Symbol.unscopables属性。

    Array.prototype[Symbol.unscopables]
    // {
    //   ...
    //   keys: true,
    //   values: true,
    // }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

      也就表示数组默认包含Symbol.unscopables属性,因此以下代码在with环境中就排除了values属性。在ES6浏览器中,将输出[],与ES5的结果一致。

    function fn(values) {
      with (values) {
        console.log(values)
      }
    }
    
    fn([])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      所以引入Symbol.unscopables,仅仅是为了解决with执行环境下的历史兼容性问题。

    严格模式

      可能你会问,ES5中严格模式不是已经禁用了with,为何还要在ES6中解决禁用语句的遗留问题?

      个人认为目前浏览器还处在支持严格和非严格两种模式的阶段,非严格模式下还是能正常运行with语句,所以始终都存在着潜在的问题。

      即由于规范的差异,导致代码的行为不一致。所以终究还是要解决掉,保证向下兼容,即使解决方式不是太友好。

    参考

    🎉 写在最后

    🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star ✨支持一下哦!

    手动码字,如有错误,欢迎在评论区指正💬~

    你的支持就是我更新的最大动力💪~

    GitHub / GiteeGitHub Pages掘金CSDN 同步更新,欢迎关注😉~

  • 相关阅读:
    如何保证接口的幂等性?
    易基因|ChIP-seq等实验揭示CHD6转录激活前列腺癌通路的关键功能 | 肿瘤耐药研究
    你写过的最蠢的代码是?
    二聚乳酸-羟基乙酸共聚物聚乙二醇 PLGA-PEG-PLGA
    Java 集合之 Queue 和 Deque
    阅读源码工具Sourcetrail
    向量检索之一:Faiss 在工业界的应用和常见问题解决
    STM32-CAN
    大模型日报2024-04-23
    6.typescript类
  • 原文地址:https://blog.csdn.net/Don_GW/article/details/126142415