Blog icon indicating copy to clipboard operation
Blog copied to clipboard

变量和类型 - Symbol的应用及实现

Open logan70 opened this issue 5 years ago • 0 comments

Symbol的应用及实现

Symbol

Symbol([description])函数会返回symbol类型的值。

  • description:可选的字符串。symbol的描述,可用于调试但不能访问symbol本身。
  • 每个从Symbol()返回的symbol值都是唯一的;
  • symbol是一种基本数据类型;
  • symbol类型唯一目的:作为对象属性的标识符。
const symbol1 = Symbol()
const symbol2 = Symbol('foo')

全局共享的Symbol

Symbol.for(key):全局symbol注册表中有与key对应的symbol则返回,否则在全局symbol注册表新建与key关联的symbol并返回。

Symbol.keyFor(symbol):获取全局symbol注册表中与某个 symbol 关联的键,没有则返回undefined

const globalSym = Symbol.for('foo')
expect(Symbol.keyFor(globalSym)).toBe('foo')

const localeSym = Symbol('bar')
expect(Symbol.keyFor(localeSym)).toBeUndefined()

Symbol特性

symbol的创建

  • 不能通过new关键字调用Symbol函数,因为禁止创建显式的 Symbol 包装器对象
expect(() => new Symbol('foo')).toThrowError(new TypeError('Symbol is not a constructor'))

expect(() => Symbol('foo')).not.toThrow()

symbol类型的识别

  • 使用typeof运算符来识别symbol类型
  • symbol是原始类型,无法使用instanceof进行识别
  • 如果想得到一个Symbol包装器对象,可以使用Object()函数。
const sym = Symbol('foo')

expect(typeof sym).toBe('symbol')
expect(sym instanceof Symbol).toBe(false)

const symObj = Object(sym)
expect(symObj instanceof Symbol).toBe(true)

symbol的类型转换

symbol类型值可显式转string类型或者boolean类型, 不能转number类型。

const sym = Symbol('foo')
expect(String(sym)).toBe('Symbol(foo)')
expect(Boolean(sym)).toBe(true)
expect(() => Number(sym))
  .toThrowError(new TypeError('Cannot convert a Symbol value to a number'))

对象symbol属性的获取

  • 对象的symbol属性在for...in迭代中不可枚举,也无法通过Object.keys/Object.getOwnPropertyNames获得。
  • 可以使用Object.getOwnPropertySymbols()对象自身的所有 Symbol 属性的数组。
  • Reflect.ownKeys()可以获取对象自身的所有可枚举、不可枚举及Symbol属性的数组。
const obj = {
  [Symbol('foo')]: 'foo',
  bar: 'bar'
}
const isSymbol = s => typeof s === 'symbol'
const hasSymbol = arr => arr.some(isSymbol)

let canGetSymbolByForIn = false
for (k in obj) {
  if (isSymbol(k)) {
    canGetSymbolByForIn = true
    break
  }
}
expect(canGetSymbolByForIn).toBe(false)

expect(hasSymbol(Object.keys(obj))).toBe(false)
expect(hasSymbol(Object.getOwnPropertyNames(obj))).toBe(false)
expect(Object.getOwnPropertySymbols(obj).map(String)).toEqual(['Symbol(foo)'])
expect(Reflect.ownKeys(obj).map(String)).toEqual(['bar', 'Symbol(foo)'])

Symbol的应用

使用Symbol作为对象属性名

对象次要的元信息属性或者不想被迭代的属性,可以使用Symbol来作为属性名,相较Object.defineProperty去指定enumerable: false比较简洁。

const META_PROP = Symbol('meta')
const obj = {
  [META_PROP]: '次要信息',
  name: 'logan',
  age: 18,
}

expect(Object.keys(obj)).toEqual(['name', 'age'])

使用Symbol代替常量

好处是不用考虑常量值重复,常量较多时比较有用。

// before
const TYPE_AUDIO = 'AUDIO'
const TYPE_VIDEO = 'VIDEO'

// after
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()

使用Symbol模拟私有属性/方法

注意: 仅用作模拟,不要尝试使用 Symbol 存储对象中需要真正私有化的值,如密码等属性,对象上所有的 Symbols 都可以直接通过 Object.getOwnPropertySymbols() 获得!

// lady.js
const AGE = Symbol('age')
const GET_AGE = Symbol('getAge')
export class Lady {
  constructor(username, age) {
    this.username = username
    this[AGE] = age
  }

  [GET_AGE]() {
    return this[AGE]
  }
}

// foo.js
import { Lady } from './lady'
const lady = new Lady('lucy', 18)

expect(lady[Symbol('age')]).toBeUndefined()
expect(() => lady[Symbol('getAge')]()).toThrowError('is not a function')

const ladyAgeKey = Object.getOwnPropertySymbols(lady)[0]
const ladyAge = lady[ladyAgeKey]
expect(ladyAge).toBe(18)

内置Symbols

内置的Symbols被用作数组、字符串等原生对象以及 JavaScript 引擎内部的方法名,这样就避免了被意外重写的可能。

介绍几个常用的内置Symbol,其余的可前往MDN-Symbol了解

Symbol.iterator

用于定义对象的迭代器,,可被for...of循环及数组展开操作符使用。

const myIterable = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3

  }
}
expect([...myIterable]).toEqual([1, 2, 3])

Symbol.hasInstance

构造函数用来识别一个对象是否为它的实例。被 instanceof 使用。

class MyArray {  
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance)
  }
}

expect([] instanceof MyArray).toBe(true)

Symbol.toPrimitive

用于定义将对象转换为原始值时的行为。

  • 执行 +obj ,会调用 obj[Symbol.toPrimitive]('number')
  • 执行 `${obj}` ,会调用 obj[Symbol.toPrimive]('string')
  • 执行 字符串连接,如'' + obj,会调用 obj[Symbol.toPrimitive]('default')
const obj = {
  [Symbol.toPrimitive](hint) {
    console.log(hint)
    return hint === 'number'
      ? 10
      : `hint is ${hint}`
  }
}

expect(+obj).toBe(10)
expect(`${obj}`).toBe('hint is string')
expect(obj + '').toBe('hint is default')

Symbol.toStringTag

用于对象的默认描述的字符串值。被Object.prototype.toString()使用。

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

expect(Object.prototype.toString.call(new Person)).toBe('[object Person]')

实现Symbol

typeof Symbol() === 'symbol'、对象symbol属性不可迭代等特性无法模拟。

我们围绕最重要的特性,也是symbol类型的唯一目的--作为对象属性的标识符来进行模拟。

// 自定义symbol对象的原型
const symbolProto = {}

// 设置对象属性时会调用toString,返回__name__属性
Object.defineProperties(symbolProto, {
  toString: generatePrivateDescriptor(function() { return this.__name__ }),
})

export default function SymbolPolyfill(description) {
  // 实现禁止使用new操作符生成Symbol
  if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor')
  // symbol描述为undefined时为空,其他情况均强制转换为字符串
  description = description === undefined ? '' : String(description)
  symbol = Object.create(symbolProto)
  return Object.defineProperties(symbol, {
    __name__: generatePrivateDescriptor(generateName(description)),
  })
}

// 生成唯一的字符串
const nameRecorder = {}
function generateName(desc) {
  let postfix = 0
  while (nameRecorder[desc + postfix]) postfix++
  nameRecorder[desc + postfix] = true
  return '@@' + desc + postfix
}

// 生成Object.defineProperty的描述对象
function generatePrivateDescriptor(value) {
  return {
    value,
    configruable: false,
    enumerable: false,
    writable: false
  }
}
// 测试
import SymbolPolyfill from './SymbolPolyfill'

const sym1 = SymbolPolyfill('foo')
const sym2 = SymbolPolyfill('foo')

const obj = {}
obj[sym1] = 1
obj[sym2] = 2

expect(sym1 in obj).toBe(true)
expect(sym2 in obj).toBe(true)
expect(obj[sym1]).not.toBe(true)

logan70 avatar Nov 14 '19 07:11 logan70