-
Notifications
You must be signed in to change notification settings - Fork 8
Description
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)