Symbol(符号)是ES6新增的数据类型。符号是原始值,且符号实例是唯一的、不可变的。符号的用途是确保对象属性使用唯一的标识符,不会发生属性冲突的危险。
1.符号的基本用法
符号是需要使用Symbol()函数初始化。因为符号本身是原始类型,所以typeof操作符对符号返回symbol。
let sym = Symbol();
console.log(typeof sym); // symbol
调用Symbol函数时,也可以传入一个字符串参数作为对Symbol的描述,将来可以通过这个字符串来调试代码。但是这个字符串参数与Symbol定义或者标识完全没有关系。
let firstSymbol = Symbol();
let secondSymbol = Symbol();
let fooFirstSymbol = Symbol('foo');
let fooSecondSymbol = Symbol('foo');
console.log(firstSymbol == secondSymbol); // false
console.log(fooFirstSymbol == fooSecondSymbol); // false
最重要的是,Symbol()函数不能与new关键字一起作为构造函数使用。这样是为了避免创建符号包装对象,像使用Boolean、String或Number那样,它们都支持构造函数且可用于初始化包含原始值的包装对象。
let myBoolean = new Boolean();
console.log(typeof myBoolean); // object
let myString = new String();
console.log(typeof myString); // object
let myNumber = new Number();
console.log(typeof myNumber); // object
let mySymbol = new Symbol();
console.log(typeof myNumber); // TypeError: Symbol is not a constructor
如果确实想使用符号包装对象,可以借用Object()函数:
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // object
2.使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例。那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。
为此需要使用Symbol.for()方法:
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,他会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新的符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooSymbol); // true
即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不相同:
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
全局注册表中的符号必须使用字符串键来创建,因此作为参数穿个Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。
let oneSymbol = Symbol.for();
let twoSymbol = Symbol.for('111');
console.log(oneSymbol, twoSymbol); // Symbol(undefined) Symbol(111)
还可以使用Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对用的字符串键。如果查询的不是全局符号,则返回undefined。如果传给Symbol.keyFor()的不是符号,则该方法抛出TypeError:
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
Symbol.keyFor(111); // TypeError: 111 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'
}
// 也可以这么写 o[s1] = 'foo val';
console.log(o); // {Symbol(foo): 'foo val'}
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o); // {Symbol(foo): 'foo val', Symbol(bar): 'bar val'}
Object.defineProperties(o, {
[s3]: {value: 'bar val'},
[s4]: {value: 'qux val'}
});
console.log(o);
// {Symbol(foo): 'foo val', Symbol(bar): 'bar val', Symbol(baz): 'bar val', Symbol(qux): 'qux val'}
4.常用内置符号
ECMAScript6引入了一批常用内置符号,试试用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一就是重新定义他们,从而改变原生结构的行为。比如,我们知道for-of循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义对象上重新定义Symbol.iterator的值,来改变for-of在迭代该对象时的行为。
这些内置符号也没有什么特别之处,他们就是全局函数Symbol的普通字符串属性,指向一个符号的实例。所有的内置符号属性都是不可写、不可枚举、不可配置的。
注意:在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator指的就是Symbol.iterator。
5.Symbol的其他知识点(本篇暂时不讲,只列出)
Symbol.asyncIterator、Symbol.hasInstance、Symbol.isConcatSpreadable、Symbol.iterator、Symbol.match、Symbol.replace、Symbol.search、Symbol.species、Symbol.split、Symbol.toPrimitive、Symbol.toStringTag、Symbol.unscopables。