# ES6 Symbol

# 前言

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

# 属性方法

  ;Symbol (opens new window) 为符号类型,属于基本数据类型之一。

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

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

Symbol() === Symbol() // false

  创建一个Symbol包装对象。

var sym = Symbol()
var object = Object(sym) // Symbol {Symbol(), description: undefined}

typeof sym // symbol
typeof object // object

# Symbol.prototype.description

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

var sym = Symbol('desc')

sym.description // desc
sym.toString() // Symbol(desc)
String(sym) // Symbol(desc)

# Symbol.for

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

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

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

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

# Symbol.keyFor

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

var s = Symbol(),
  y = Symbol.for(),
  m = Symbol.for('desc')

Symbol.keyFor(s) // undefined
Symbol.keyFor(y) // 'undefined'
Symbol.keyFor(m) // 'desc'

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

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

function inGlobalRegistry(sym) {
  return !!Symbol.keyFor(sym)
}

# Symbol

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

# Symbol.hasInstance

  ;instanceof (opens new window) 用于检测构造函数的原型是否在实例对象的原型链上。

function F() { }
var f = new F()

f instanceof F // true
f instanceof Object // true

  函数实现。

function instanceOf(object, constructor) {
  // or object.__proto__
  while ((object = Object.getPrototypeOf(object))) {
    if (object === constructor.prototype) {
      return true
    }
  }

  return false
}

instanceOf(String, Object) // true

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

function F() { }
var f = new F()

f instanceof F // true
F[Symbol.hasInstance](f) // true

  自定义instanceof

var F = {
  [Symbol.hasInstance](v) {
    return v % 2 === 0
  },
}

1 instanceof F // false
2 instanceof F // true

# Symbol.isConcatSpreadable

  ;Symbol.isConcatSpreadable (opens new window) 即数组或类数组在被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]

  而类数组默认不可展开。

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]

# 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

  如何做到的呢?

  以下为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
  }
}

  运行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

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

  有何作用呢?

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

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

# Symbol.match

  ;Symbol.match (opens new window)String.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]

# Symbol.replace

  ;Symbol.replace (opens new window)String.prototype.replace在语言内部将执行RegExp.prototype[Symbol.replace]

var regexp = /llo/, s = 'hello'

s.replace(regexp, 'he') // hehe
regexp[Symbol.replace](s, 'he') // hehe

  ;Symbol.search (opens new window)String.prototype.search在语言内部将执行RegExp.prototype[Symbol.search]

var regexp = /llo/, s = 'hello'

s.search(regexp) // 2
regexp[Symbol.search](s) // 2

# Symbol.split

  ;Symbol.split (opens new window)String.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']

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

# Symbol.iterator

  ;Symbol.iterator (opens new window) 为对象部署迭代器,可被用于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']

# Symbol.toPrimitive

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

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

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

  ;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

# 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] + {}结果为例。

  实际形如 x + y (opens new window) 的表达式,将分别对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]'

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

# Symbol.toStringTag

  ;Symbol.toStringTag (opens new window) 用于向Object.prototype.toString提供标签。

var object = {
  [Symbol.toStringTag]: 'Hello',
}

object.toString() // [object Hello]
Object.prototype.toString.call(object) // [object Hello]

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

# Symbol.unscopables

# with

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

var foo = 1

function fn() {
  console.log(foo) // 1
}

fn()

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

var foo = 1

function fn() {
  var object = { foo: 2 }

  with (object) {
    console.log(foo) // 2
  }
}

fn()

  因此 with (opens new window) 作用非常简单,即扩展了语句的作用域。

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

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()

  ;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()

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

function fn(x, y) {
  with (x) {
    console.log(y)
  }
}

fn({ y: 1 }) // 1
fn({}, 2) // 2

# 兼容性

  在框架 extjs (opens new window) 中存在类似如下的代码。

function fn(values) {
  with (values) {
    console.log(values)
  }
}

fn([])

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

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

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

  呐,问题就严重咯~

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

  思考下怎么解决呢?

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

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

var foo = 1

function fn() {
  var object = {
    foo: 2,
    [Symbol.unscopables]: {
      foo: true,
    },
  }

  with (object) {
    console.log(foo) // 1
  }
}

fn()

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

Array.prototype[Symbol.unscopables]
// {
//   ...
//   keys: true,
//   values: true,
// }

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

function fn(values) {
  with (values) {
    console.log(values)
  }
}

fn([])

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

# 严格模式

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

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

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

# 参考

# 🎉 写在最后

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

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

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

GitHub (opens new window) / Gitee (opens new window)GitHub Pages (opens new window)掘金 (opens new window)CSDN (opens new window) 同步更新,欢迎关注😉~

最后更新时间: 8/12/2022, 10:20:59 AM