Blog
Blog copied to clipboard
变量和类型 - Symbol的应用及实现
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)