learn-typescript

Symbol

概述

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。于是 ES6 中引入了一种新的 原始数据类型 —— Symbol,表示独一无二的值。 至此 JavaScript 共有了 7 种数据类型:

typeof 的特殊值:

创建Symbol值

Symbol值通过 Symbol() 函数生成。注意: Symbol() 函数前不能加 new 命令,因为 Symbol 类型是原始数据类型,不是对象。

let s = Symbol()

console.log(typeof s)  // "symbol"

Symbol值是独一无二的,所以Symbol() 函数支持传入字符串参数作为对该Symbol值的描述,用来区分。

let sym1 = Symbol('foo')
let sym2 = Symbol('bar')

console.log(sym1)  // Symbol(foo)
console.log(sym2)  // Symbol(bar)

console.log(sym1.toString())   // "Symbol(foo)"
console.log(sym1.toString())   // "Symbol(bar)"

注意: Symbol()函数的参数只表示对当前 Symbol 值的描述,因此即使是相同的参数,生成的 Symbol 值也是不同的。

let s1 = Symbol()
let s2 = Symbol()

console.log(s1 === s2)  // false

let s3 = Symbol('foo')
let s4 = Symbol('foo')

console.log(s3 === s4)  // false

Symbol类型的转换

Symbol值可以转换为字符串或布尔值,不能转换为数值,并且不能直接与其他类型的值进行运算,必须显式转换为字符串后进行计算:

let sym = Symbol('sym')

// 可以转换为字符串或布尔值
String(sym)  // "Symbol(sym)"
sym.toString()   // "Symbol(sym)"
Boolean(sym)  // true
!sym  // false


// 不能转换为数值
Number(sym)  // 错误

// 不能直接与其他类型运算,必须显示转换为字符串
'symbol is:' + sym             // 错误
sym + 1                        // 错误

'symbol is:' + sym.toString()  // ok
String(sym) + 1                // ok

Symbol.prototype.description

ES2019 提供了一个新的属性 description 直接返回 Symbol值的描述:

const sym = Symbol('foo')

sym.description  // "foo"

Symbol使用场景

Symbol值作为属性名

由于 Symbol值是独一无二的,所以可以用来作为对象的属性名,保证属性名的唯一性,防止被意外修改或覆盖。

const sym_name = Symbol('sym_name')
const sym_foo = Symbol('sym_foo')

let obj = {
  [sym_name]: 'symValue',
  [sym_foo]() {
    console.log('foo')
  }
}
console.log(obj[sym_name])   // "symValue"
obj[sym_foo]()               // "foo"

Symbol值作为常量

Symbol还可以用于定义一组常量,保证这组常量的值都不相等。

const COLOR_RED = Symbol()
const COLOR_GREEN = Symbol()

function getColor(color: symbol) {
  switch (color) {
    case COLOR_GREEN:
      return 'green'
    case COLOR_RED:
      return 'red'
    default:
      throw new Error('unknown color')
  }
}

遍历Symbol类型的属性名

Symbol值作为属性名,无法被 for...of, for...in, Object.keys(), Object.getOwnPropertyNames(), JSON.stringify() 遍历:

const foo = Symbol('foo')

const obj = {} as any
obj[foo] = 'bar'

for (const key in obj) {
  console.log(obj)       // 无输出
}

console.log(Object.keys(obj))       // []
console.log(Object.getOwnPropertyNames(obj)) // []
console.log(JSON.stringify(obj))    // "{}"

但是 Symbol属性名也不是私有属性,可以通过 Object.getOwnPropertySymbols() 方法获取指定对象的所有Symbol属性名:

console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol(foo) ]

Symbol.for()和Symbol.keyFor()

有时想要重新使用同一个Symbol值,此时可以使用 Symbol.for() 方法。
Symbol.for(arg: string) 接受一个字符串参数,查找是否已存在该参数为名称的 Symbol值,如果已存在就返回该 Symbol值, 没有则以该参数为名称创建新的 Symbol值,并将其注册到全局环境供之后查找。

注意: 使用 Symbol() 创建的值是无法被 Symbol.for() 搜索到的,因为它没有被注册到全局环境。

const sym_1: symbol = Symbol('sym_1')      // 没有注册到全局环境,除了 sym_1 变量之外无法获取该 Symbol值
const sym_2: symbol = Symbol.for('sym_2')  // 没有查找到,新建一个并注册到全局环境
const sym_3: symbol = Symbol.for('sym_2')  // 查找到了,返回该值

console.log(sym_1 === sym_2) // false
console.log(sym_2 === sym_3) // true

Symbol.keyFor() 能够返回一个已登记的 Symbol值的 描述字符串(description属性):

注意: 使用 Symbol() 创建的值是无法被 Symbol.keyFor() 搜索到的,因为它没有被注册到全局环境。

const sym_1: symbol = Symbol('sym_1')    
const sym_2: symbol = Symbol.for('sym_2')
const sym_3: symbol = Symbol.for('sym_2')

console.log(Symbol.keyFor(sym_1)) // undefined
console.log(Symbol.keyFor(sym_3)) // "sym_2"

内置的Symbol值

ES6 提供了 11 个内置的 Symbol值,指向语言内部使用的方法。

1.Symbol.hasInstance

当某个对象使用 instanceof 运算符判断是否为某个类的实例时,会调用该方法。例如: foo instanceof Foo 在语言内部实际调用的是 Foo[Symbol.hasInstanceof](foo)

class Foo {
  static [Symbol.hasInstance](obj: any) {
    return obj instanceof String
  }
}
console.log(new String('123') instanceof Foo) // true
console.log([1, 2, 3] instanceof Foo) // false

2.Symbol.isConcatSpreadable

指示该对象用于 Array.prototype.concat() 时是否可以展开。


// 数组, 默认展开
let arr: any = [1, 2]

console.log(arr[Symbol.isConcatSpreadable]) // undefined,数组默认可以展开
console.log([3, 4].concat(arr)) // [ 3, 4, 1, 2 ]

arr[Symbol.isConcatSpreadable] = false // 设置为false,不可以展开
console.log(arr[Symbol.isConcatSpreadable]) // false
console.log([3, 4].concat(arr)) // [ 3, 4, [ 1, 2, [Symbol(Symbol.isConcatSpreadable)]: false ] ]


// 类数组, 默认不展开
let arrLike: any = { 0: 'a', 1: 'b', length: 2 }

console.log(arrLike[Symbol.isConcatSpreadable]) // undefined, 类数组默认不展开
console.log([3, 4].concat(arrLike)) // [ 3, 4, { '0': 'a', '1': 'b', length: 2 } ]

arrLike[Symbol.isConcatSpreadable] = true // 设置为 true, 可以展开
console.log(arrLike[Symbol.isConcatSpreadable]) // true
console.log([3, 4].concat(arrLike)) // [ 3, 4, 'a', 'b' ]

Symbol.isConcatSpreadable 也可以定义在类里面:

class A1 extends Array {
  constructor() {
    super()
    ;(this as any)[Symbol.isConcatSpreadable] = true
  }
}

class A2 extends Array {
  constructor() {
    super()
  }

  get [Symbol.isConcatSpreadable](): boolean {
    return false
  }
}

let a1 = new A1()
a1[0] = 'A'
a1[1] = 'B'

let a2 = new A2()
a2[0] = 3
a2[1] = 4

console.log([1, 2].concat(a1).concat(a2))  // [ 1, 2, 'A', 'B', [ 3, 4 ] ]

注意: A1Symbol.isConcatSpreadable 属性设置在实例上,而 A2 中设置在了类的原型上。

3.Symbol.species

指向一个构造函数,创建衍生对象时使用该属性。

class MyArr<T> extends Array<T> {

}

let a = new MyArr(1, 2, 3)
let b = a.map(item => item)
let c = a.filter(item => item > 1)

console.log(b instanceof MyArr) // true
console.log(c instanceof MyArr) // true

上面例子中, aMyArr 的实例,bc 是衍生对象,你可能认为 bc 都是数组方法生成的,应该是 Array 的实例,但是 bcMyArr 的实例。

Symbol.species 属性可以改变创建衍生对象使用的构造函数:

class MyArr<T> extends Array<T> {
  static get [Symbol.species]() {
    return Array
  }
}

let a = new MyArr(1, 2, 3)
let b = a.map(item => item)
let c = a.filter(item => item > 1)

console.log(b instanceof MyArr) // false
console.log(c instanceof Array) // true

4.Symbol.match

当调用 str.match(obj) 时使用,相当于 obj[Symbol.match](this)

class SymbolMatchDemo {
  [Symbol.match](str: string) {
    return str.indexOf('w')
  }
}

let str = 'hello world'
(str as any).match(new SymbolMatchDemo()) // 6

5.Symbol.replace

当调用 str.replace(old, newStr) 时使用, 相当于 old[Symbol.replace](this, newStr)

class SymbolReplaceDemo {
  [Symbol.replace](str: string, newStr: string) {
    console.log(str, newStr)
  }
}

let str = 'hello world'
(str as any).replace(new SymbolReplaceDemo(), 'xyz')  // "hello world xyz"

6.Symbol.search

当调用 str.search(obj) 时使用, 相当于 obj[Symbol.search](this)

class SymbolSearchDemo {
  value: string = 'llo';
  [Symbol.search](str: string) {
    return str.indexOf(this.value)
  }
}

let str = 'hello world'
console.log(str.search(new SymbolSearchDemo()))  // 2

7.Symbol.split

当调用 str.split(seperator, limit) 时使用,相当于 seperator[Symbol.split](this, limit)

class SymbolSplitDemo {
  separator: string = 'ld';
  [Symbol.split](str: string) {
    let index: number = str.indexOf(this.separator)
    if (index === -1) {
      return str
    } else {
      return [
        str.substring(0, index),
        str.substring(index + this.separator.length)
      ]
    }
  }
}

console.log((ss as any).split(new SymbolSplitDemo())) // [ 'hello wor', '' ]

8.Symbol.iterator

返回该对象的默认遍历器方法,用于 for...of 遍历时调用。

class SymbolIteratorDemo {
  *[Symbol.iterator]() {
    yield 'a'
    yield 'b'
    yield 'c'
  }
}

let retArr = []
for (const item of new SymbolIteratorDemo()) {
  retArr.push(item)
}
console.log(retArr)  // [ 'a', 'b', 'c' ]

9.Symbol.toprimitive

指定该对象被转为原始类型时返回的值。接受一个字符串参数表示当前运算模式,一共有三种模式:

class SymbolToPrimitiveDemo {
  constructor(public numValue: number, public strValue: string) {}

  [Symbol.toPrimitive](pattern: string) {
    switch (pattern) {
      case 'number':
        return this.numValue
      case 'string':
        return this.strValue
      case 'default':
        return 'default'
      default:
        throw new Error('unknown pattern')
    }
  }
}

const stpd = new SymbolToPrimitiveDemo(123, 'abc')
2 * (stpd as any)          // 246
2 + (stpd as any)          // "2default"
'hello, ' + (stpd as any)  // "hello, default"
String(stpd)               // "abc"
(stpd as any) == 'default' // true

10.Symbol.toStringTag

用于定制 toString() 方法返回的字符串中的对象的类型,例如 [object Array] 中的 Array

class SymbolToStringTagDemo {
  get [Symbol.toStringTag]() {
    return 'SymbolToStringTag'
  }
}

console.log(new SymbolToStringTagDemo().toString())  // [object SymbolToStringTag]

11.Symbol.unscopables

指示使用 with 关键字时,该对象的哪些属性会被 with 环境排除。

严格模式无法使用 with

class SymbolUnscopablesDemo {
  foo() {
    return 'Symbol.unscopables'
  }
  bar() {
    return 123
  }
  get [Symbol.unscopables]() {
    return {
      foo: true
    }
  }
}

const foo = () => 'outername'
const bar = () => 321

with (SymbolUnscopablesDemo.prototype) {
  console.log(foo(), bar())               // "outername" 123
}