Symbol类型
Symbol(符号)是ECMAScript新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生冲突的危险。
尽管和私有属性有点类似,但符号并不是为了提供私有属性的行为增加的。相反符号是为了创建唯一记号,进而用作非字符串形式的对象属性。
1、符号的基本用法
符号需要使用Symbol()函数初始化。因符号本身是原始类型,所以typeof操作符对符号返回symbol。
例:
let sym=Symbol();
console.log(typeof sym) //symbol
调用Symbol()函数时,也可以传入一个字符串作为对符号的描述,可以通过这个字符串来调试代码。但这个字符串参数与符号定义或标识完全无关。
let Gsymbol=Symbol('foo')
let Osymbol=Symbol('foo')
console.log(Gsymbol==Osymbol) //false
符号没有字面量语法,这也它们发挥作用的关键。按规范,只要创建Symbol()实例并将其用作对象的新属性,就可以确保它不会覆盖已有的对象属性,无论是符号还是字符串属性。
let Gsymbol=Symbol()
console.log(Gsymbol) //Symbol()
let Fsymbol=Symbol('foo')
console.log(Fsymbol) //Symbol(foo)
console.log(Gsymbol) //Symbol() Gsymbol没有被覆盖
最重要的是Symbol()函数不能与new关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象。
例:
let mySymbol=new Symbol() //报错:TypeError: Symbol is not a constructor
如果想用符号包装对象,可以借用Object()函数。
例:
let mySymbol=Symbol()
let mydefinedSymbol=Object(mySymbol)
console.log(typeof mydefinedSymbol) //'object'
2、使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号包装对象。
为此需要使用Symbol.for()方法:
let golbalSymbol=Symbol.for('foo')
console.log(typeof golbalSymbol) //symbol
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局进行时注册表,发现不存在对应符号,于是会生成一个新的符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let GolbalSymbol=Symbol.for('foo') //创建新符号
let otherSymbol=Symbol.for('foo') //重用已有符号
console.log(GolbalSymbol==otherSymbol) //true
即使采用相同的符号描述,使用Symbol()和使用Symbol.for()定义的符号也并不相同
let GolbalSymbol=Symbol('foo')
let otherSymbol=Symbol.for('foo')
console.log(GolbalSymbol==otherSymbol) //false
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()任何值都会被转换为字符串,此外,注册表中使用的键同时也会被用作符号描述。
let emptySymbol=Symbol.for()
console.log(emptySymbol) //Symbol(undefined)
还可以用Symbol.keyFor()来查询注册表,这个方法接收符号,返回该全局对应的字符串键。如果查询的不是全局符号,则返回undefined。
// 创建全局符号
let s=Symbol.for('bar')
console.log(Symbol.keyFor(s)) //foo
// 创建普通符号
let s2=Symbol('bar')
console.log(Symbol.keyFor(s2)) //undefined
如果Symbol.keyFor()的不是符号,则该方法抛出TypeError:
Symbol.keyFor(123) //TypeError: 123 is not a symbol
3、使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这包括了对象字面量属性和Object.defineProperty()//Object.defineProperties()定义的属性。对象字面量只能在计算属性方法中使用符号作为属性。
例:
let s1=Symbol('foo')
s2=Symbol('bar')
s3=Symbol('baz')
s4=Symbol('qux')
// 符号定义属性
let o={
[s1]:'foo val'
}
console.log(o)
//输出结果为:{Symbol(foo): 'foo val'}
// Object.defineProperty定义属性
Object.defineProperty(o,s2,{value:'bar val'})
console.log(o)
//输出结果为:{Symbol(foo): 'foo val', Symbol(bar): 'bar val'}
// Object.defineProperties定义属性
Object.defineProperties(o,{
[s3]:{value:'baz val'},
[s4]:{value:'qux val'}
})
console.log(o)
//输出结果为:{Symbol(foo): 'foo val', Symbol(bar): 'bar val', Symbol(baz): 'baz
val', Symbol(qux): 'qux val'}
类似Object.getOwnProperNames()返回对象实例的常规性数组,Object.getOwnPropertySymbols()返回对象的实例的符号属性数组,这两个方法返回值彼此互斥,Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键。
例:
let s1=Symbol('foo')
let s2=Symbol('bar')
let o={
[s1]:'foo val',
[s2]:'bar val',
baz:'baz val',
qux:'qux val'
}
//返回对象实例的常规性数组: ['baz', 'qux']
console.log(Object.getOwnPropertyNames(o))
//返回对象的实例的符号属性数组:[Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertySymbols(o))
//返回同时包含常规和符号属性描述符的对象:{baz: {…}, qux: {…}, Symbol(foo): {…},
Symbol(bar): {…}}
console.log(Object.getOwnPropertyDescriptors(o))
//返回两种类型的键:['baz', 'qux', Symbol(foo), Symbol(bar)]
console.log(Reflect.ownKeys(o))
4、常用内置符号
ES6也引入了一批常用内置符号,用于暴露语言行为。开发者可以直接访问、重写或模拟这些行为。这些内容符号都以Symbol工厂字符串形式存在。这些内置符号最重要用途之一是重新定义它们,从而改变原生结构行为。比如我们知道for-of循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义对象上重新定义Symbol.iterator的值,来改变for-of在迭代该对象时的行为。
这些内置符号没有什么特别之处,它们就是全局变量Symbol的普通字符串属性,指向一个符号的实例。所以内置符号属性都是不可写、不可枚举、不可配置的。