Symbol(符号)是ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
1.符号的基本用法
符号需要使用Symbol()函数初始化。因为符号本身是原始类型,所以typeof操作符对符号返回symbol。调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关
let genericSymbol = Symbol(); let otherGenericSymbol = Symbol(); let fooSymbol = Symbol('foo'); let otherFooSymbol = Symbol('foo'); console.log(genericSymbol == otherGenericSymbol); // false console.log(fooSymbol == otherFooSymbol); // false
符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。
最重要的是,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" letmySymbol=new Symbol();// TypeError: Symbol is not a constructor
2.使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。为此,需要使用Symbol.for()方法:
let fooGlobalSymbol = Symbol.for('foo'); console.log(typeof fooGlobalSymbol); // symbol
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号 let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号 console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
还可以使用Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined。
// 创建全局符号 let s = Symbol.for('foo'); console.log(Symbol.keyFor(s)); // foo // 创建普通符号 let s2 = Symbol('bar'); console.log(Symbol.keyFor(s2)); // undefined
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: '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.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnProperty-Symbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnProperty-Descriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键:
let s1 = Symbol('foo'), s2 = Symbol('bar'); let o = { [s1]: 'foo val', [s2]: 'bar val', baz: 'baz val', qux: 'qux val' }; console.log(Object.getOwnPropertySymbols(o)); // [Symbol(foo), Symbol(bar)] console.log(Object.getOwnPropertyNames(o)); // ["baz", "qux"] console.log(Object.getOwnPropertyDescriptors(o)); // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} console.log(Reflect.ownKeys(o)); // ["baz", "qux", Symbol(foo), Symbol(bar)]
4.常用内置符号
ECMAScript 6也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以Symbol工厂函数字符串属性的形式存在。这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义对象上重新定义Symbol.iterator的值,来改变for-of在迭代该对象时的行为。这些内置符号也没有什么特别之处,它们就是全局函数Symbol的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
注意 在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator指的就是Symbol.iterator。
Symbol.asyncIterator
这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator。由for-await-of语句使用”。换句话说,这个符号表示实现异步迭代器API的函数。
Symbol.hasInstance
这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof操作符使用”。instanceof操作符可以用来确定一个对象实例的原型链上是否有原型。
Symbol.isConcatSpreadable
这个符号作为一个属性表示“一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素”。ES6中的Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。
Symbol.iterator
这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of语句使用”。换句话说,这个符号表示实现迭代器API的函数。
Symbol.match
这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由String.prototype.match()方法使用”。String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数
Symbol.replace
这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由String.prototype.replace()方法使用”。String.prototype.replace()方法会使用以Symbol.replace为键的函数来对正则表达式求值。
Symbol.search
这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由String.prototype.search()方法使用”。String.prototype.search()方法会使用以Symbol.search为键的函数来对正则表达式求值。
Symbol.species
这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用Symbol.species定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义
Symbol.split
这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。String.prototype. split()方法会使用以Symbol.split为键的函数来对正则表达式求值。
Symbol.toPrimitive
这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。
Symbol.toStringTag
这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()使用”。通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为”Object”。
Symbol.unscopables
这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with环境绑定中排除”。设置这个符号并让其映射对应属性的键值为true,就可以阻止该属性出现在with环境绑定中