Symbol符号是ES6新增的原始类型数据,其符号实例是唯一的、不可变的。
Symbol用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
所以,Symbol的主要用途是创建唯一标记。
初始化
使用symbol函数时,是可以传一个字符串参数的,他会作为symbol的描述(description),将来可以通过他来调试代码。
1 let sym = Symbol();
2 // 支持传递参数
3 let asym = Symbol('a');
但是,这个字符串参数与symbol 定义或标识完全无关:
1 let sym = Symbol();
2 // 支持传递参数
3 let asym = Symbol('a');
4 let bsym = Symbol('a');
5
6 console.log(asym==bsym);//flase
symbol没有字面量语法,这也是它们发挥作用的关键。
只要创建symbol实例并将其用作新对象的属性,就可以包装它不会覆盖已有的对象属性,无论是符号symbol属性还是字符串属性
重点:Symbol()函数不能与new关键字一起作为构造函数使用。这样做是为了避免创建符号symbol包装对象。(而Boolean、String或Number都支持构造函数且可用于初始化包含原始值的包装对象)
1 let a =new Symbol()//TypeError: Symbol is not a constructor
使用全局符号注册表
如果运行时的不同部分需要共享和重用符号symbol实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号symbol。
为此,需要使用Symbol.for()方法:
1 let b = Symbol.for('foo')
2 console.log(typeof b)//symbol
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
1 let b = Symbol.for('foo');
2 let c = Symbol.for('foo');//重用已有符号
3 console.log(b===c)//true
且只有通过Symbol.for()方法创建的符号才能添加到注册表中,直接用Symbol()创建的符号,即使描述相同但也不等同于在全局注册表中定义的符号。
1 let b = Symbol.for('foo');
2 let d = Symbol('foo')
3 console.log(b===d)//false
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()的任何值都会被转换为字符串。此外,注册表中的键同时也会被用作符号描述。
1 let a = Symbol.for()
2 console.log(a)//Symbol(undefined) 初步判断注册表中的字符串键是属于变量类型的
3
4 let c = Symbol.for({})
5 let b = Symbol.for(undefined)
6
7 console.log(b)//Symbol(undefined)
8 console.log(a===b)//true
9 console.log(c)//Symbol([object Object])
10 //因为传入的任何值都会被转换为字符串
还可以使用Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined。
1 let b = Symbol.for('foo');
2 let d = Symbol('foo')
3 console.log(Symbol.keyFor(b))//foo
4 console.log(Symbol.keyFor(d))//undefinded
如果传给keyFor的不是符号类型,则该方法抛出TypeError
Symbol.keyFor(123)//TypeError: 123 is not a symbol
使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。
1 let s1 = Symbol('foo'),
2 s2 = Symbol('bar'),
3 s3 = Symbol('baz'),
4 s4 = Symbol('qux');
5
6 let o = {
7 [s1]: 'foo val'
8 }
9 //也可以 o[s1]='foo val'
10 console.log(o)//{Symbol(foo): foo val}
11
12 Object.defineProperty(o, s2, { value: 'bar val' })
13
14 console.log(o);//{Symbol(foo): foo val,Symbol(bar): bar val}
15
16 Object.defineProperties(o, {
17 [s3]: { value: 'baz val' },
18 [s4]: { value: 'qux val' }
19 });
20
21 console.log(o)
22 // Symbol(foo): "foo val"
23 // Symbol(bar): "bar val"
24 // Symbol(baz): "baz val"
25 // Symbol(qux): "qux val"
Object.getOwnPropertyNames() 返回对象实例的常规属性数组, Object.getOwnPropertySymbols() 返回对象实例的符号属性数组。这两个方法的返回值互斥。
Object.getOwnPropertyDescriptors() 会返回常规和符号属性描述符的对象。
Relect.ownKeys() 会返回两种类型的键:
1 let o ={
2 [s1]:'foo val',
3 [s2]:'bar val',
4 baz:'baz val',
5 qux:'qux val'
6 };
7
8 console.log(Object.getOwnPropertySymbols(o))
9 //[Symbol(foo), Symbol(bar)]
10 console.log(Object.getOwnPropertyNames(o))
11 //['baz', 'qux']
12 console.log(Object.getOwnPropertyDescriptors(o))
13 //{baz: {…}, qux: {…}, Symbol(foo): {…}, Symbol(bar): {…}}
14 console.log(Reflect.ownKeys(o))
15 //['baz', 'qux', Symbol(foo), Symbol(bar)]
常用内置符号
ECMAScript6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都已Symbol工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一就是重新定义它们,从而改变原生结构的行为。
这些内置符号也没有什么特别之处,它们就是全局函数Symbol的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
Symbol.hasInstance
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象时它的实例。由instanceof操作符使用”。instanceof操作符可以用来确定一个对象实例的原型链上是否有原型。instanceof的典型使用场景如下:
1 function Foo(){}
2 let f = new Foo()
3 console.log(f instanceof Foo)//true
4
5 class Bar{}
6 let b = new Bar();
7 console.log(b instanceof Bar)//true
在ES6中,instanceof操作符会使用Symbol.hasInstance函数来确定关系。以Symbol.hasInstance为键的函数会执行同样的操作,只是操作数对调了一下:
1 function Foo(){}
2 let f = new Foo()
3 console.log(Foo[Symbol.hasInstance](f))//true
4
5 class Bar{}
6 let b = new Bar()
7 console.log(Bar[Symbol.hasInstance](b))//true
这个属性定义在Function的原型上,因此默认在所有函数和类上都可以调用。由于instanceof操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数:
1 class Bar{}
2 class Baz extends Bar{
3 static [Symbol.hasInstance](){
4 return false;
5 }
6 }
7
8 let b = new Baz()
9 console.log(Bar[Symbol.hasInstance](b))//true
10 console.log(b instanceof Bar) //true
11 console.log(Baz[Symbol.hasInstance](b))//false
12 console.log(b instanceof Baz) //false
Symbol.isConcatSpreadable
根据ECMAScript规范,这个符号作为一个属性表示“一个布尔值,如果是true,则意味着对象应该用Array.prototype.concat()打平其数组元素”。ES6中的Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcatSpreadable的值可以修改这个行为。
数组对象默认情况下会被打平到已有的数组,false或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾,true或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象在Symbol.isConcatSpreadable被设置为true的情况下将被忽略(不加入数组)。****
1 let initial = ['foo']
2 let array = ['bar']
3 console.log(array[Symbol.isConcatSpreadable])//undefined
4 //未设置时这个对象方法时 为undefined
5 console.log(initial.concat(array))//['foo','bar']
6 array[Symbol.isConcatSpreadable]=false
7 console.log(initial.concat(array))//['foo',Array(1)]
8
9 let arraylikeObject = {length:1,0:'baz'}
10 console.log(arraylikeObject[Symbol.isConcatSpreadable])//undefinded 同上
11 console.log(initial.concat(arraylikeObject))//['foo',{...}]
12 arraylikeObject[Symbol.isConcatSpreadable]=true
13 console.log(initial.concat(arraylikeObject))//['foo','baz']
14
15 let otherObject = new Set().add('qux')
16 console.log(otherObject[Symbol.isConcatSpreadable])//undefinded
17 console.log(initial.concat(otherObject))//['foo',Set(1)]
18 otherObject[Symbol.isConcatSpreadable]=true
19 console.log(initial.concat(otherObject))//['foo'] 因为不是类数组对象symbol.isConcatSpreadabe设置为true时会被忽略
Symbol.iterator
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of语句使用”。换句话说,这个符号表示实现迭代器API的函数。
for-of循环这样的语言结构会利用这个函数执行迭代操作。循环时它们会调用以Symbol.iterator为键的函数,并默认这个函数会返回一个实现迭代器API的对象。很多时候,返回对象是实现该API的Generator:
1 class Foo{
2 *[Symbol.iterator](){}
3 }
4
5 let f = new Foo()
6 console.log(f[Symbol.iterator]())
7 //Genatator{<suspaneded>}
技术上,这个由Symbol.iterator函数生成的对象应该通过其next()方法陆续返回值。可以通过显式地调用next()方法返回,也可以隐式地通过生成器函数返回:
1 class Emitter {
2 constructor(max) {
3 this.max = max;
4 this.idx = 0;
5 }
6 *[Symbol.iterator]() {
7 while (this.idx < this.max) {
8 yield this.idx++;
9 }
10 }
11 }
12
13 function count() {
14 let emitter = new Emitter(5);
15 for(const x of emitter){
16 console.log(x);
17 }
18 }
19 count();
Symbol.match
根据ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。有String.prototype.match()方法使用”。String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:
1 console.log(RegExp.prototype[Symbol.match])
2 //f [Symbol.match(){[native code]}]
3
4 console.log('foobar'.match(/bar/));
5 //['bar', index: 3, input: 'foobar', groups: undefined]
给这个方法传入非正则表达式值会导致该值被转换为RegExp对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义Symbol.match函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制:
1 class FooMatcher{
2 static[Symbol.match](target){
3 return target.includes('foo');//重写match 让match直接使用参数target 判断是否含有字符串'foo'返回bool值
4 }
5 }
6 console.log('foobar'.match(FooMatcher))//true
7 console.log('bar'.match(FooMatcher))//false
8
9 class StringMatcher{
10 constructor(str){
11 this.str = str
12 }
13 [Symbol.match](target){
14 return target.includes(this.str)
15 }
16 }
17 console.log('foobar'.match(new StringMatcher('foo')))//true
18 console.log('barbaz'.match(new StringMatcher('qux')))//true
Symbol.replace
根据ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由String.prototype.replace()方法使用"。String.prototype.replace()方法会使用以Symbol.replace为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:
1 class FooReplacer{
2 static[Symbol.replace](target,replacement){
3 return target.split('foo').join(replacement)//将foo替换为qux
4 }
5 /*用split方法将字符串分割为两(或多个)个数组(符合匹配条件的为空数组,不符合的原样保存为数组)
6 然后用join将数组转换为字符串并传入参数作为连接符,以此达到替换且不影响原数组的目的*/
7 }
8
9 console.log('barfoobaz'.replace(FooReplacer,'qux'))
10 //"barquxbaz"
11
12 class StringReplacer{
13 constructor(str){
14 this.str=str//获取当前参数作为匹配的字符串参数
15 }
16 [Symbol.replace](target,replacement){
17 return target.split(this.str).join(replacement)//将上面获取的字符串参数替换为replacement参数的字符串
18 }
19 }
20
21 console.log('barfoobaz'.replace(new StringReplacer('foo'),'qux'))
22 //replace方法中调用了StringReplacer 获得一个作为函数replace()方法的函数并且传入'foo'作为str 然后再传入 qux 作为替换字符串 完成替换
23 //"barquxbaz"
Symbol.species
根据ECMAScript规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用Symbol.species定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义:
1 class Bar extends Array { }
2 class Baz extends Array {
3 static get [Symbol.species]() {
4 return Array;
5 }
6 }
7
8 let bar = new Bar();
9 console.log(bar instanceof Array); // true
10 console.log(bar instanceof Bar); // true
11 bar = bar.concat('bar');
12 console.log(bar instanceof Array); // true
13 console.log(bar instanceof Bar); // true
14
15 let baz = new Baz();
16 console.log(baz instanceof Array); // true
17 console.log(baz instanceof Baz); // true
18 baz = baz.concat('baz');
19 console.log(baz instanceof Array); // true
20 console.log(baz instanceof Baz); // false 因为用Array覆盖了返回值所以返回为false
Symbol.split
根据ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。String.prototype.split()方法会使用以Symbol.split为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:
1 console.log(RegExp.prototype[Symbol.split]);
2 // ƒ [Symbol.split]() { [native code] }
3 console.log('foobarbaz'.split(/bar/));
4 // ['foo', 'baz']
给这个方法出啊好入非正则表达式值会导致该值被转换为RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.split函数以取代默认对正则表达式求值的行为,从而让split()方法使用费正则表达式实例。Symbol.split函数接收一个函数,就是调用match()方法的字符串实例。返回的值没有限制:
1 class FooSplitter {
2 static [Symbol.split](target) {
3 return target.split('foo');//设置以字符串'foo'作为分隔符将字符串分割为两个或多个数组
4 }
5 }
6 console.log('barfoobaz'.split(FooSplitter));
7 // ["bar", "baz"]
8 class StringSplitter {
9 constructor(str) {
10 this.str = str;//获取参数 将得到的参数的值设置为this.str的值
11 }
12 [Symbol.split](target) {
13 return target.split(this.str);//设置以this.str的值作为分隔符将字符串分割为两个或多个数组
14 }
15 }
16 console.log('barfoobaz'.split(new StringSplitter('foo')));
17 // ["bar", "baz"]
Symbol.toPrimitive
根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用”。很多内置操作都会尝试强制对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的Symbol.toPrimitive属性上定义一个函数可以改变默认行为。
根据提供给这个函数的参数(string、number或default),可以控制返回的原始值:
1 class Foo { }
2 let foo = new Foo();
3 console.log(3 + foo); // "3[object Object]"
4 console.log(3 - foo); // NaN
5 console.log(String(foo)); // "[object Object]"
6 class Bar {
7 constructor() {
8 this[Symbol.toPrimitive] = function (hint) {
9 switch (hint) {
10 case 'number':
11 return 3;
12 case 'string':
13 return 'string bar';
14 case 'default':
15 default:
16 return 'default bar';
17 }
18 }
19 }
20 }
21 let bar = new Bar();
22 console.log(3 + bar); // "3default bar"
23 console.log(3 - bar); // 0
24 console.log(String(bar)); // "string bar"
Symbol.toStringTag
根据ECMAScript规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。有内置方法Object.prototype.toString()使用”。
通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为“Object”。内置类型(Symbol的内置类型) 已经指定了这个值,但自定义类实例还需要明确定义(如果调用因为没有设置,会返回undefined):
let s = new Set();
console.log(s); // Set(0) {}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set
class Foo { }
let foo = new Foo();
console.log(foo); // Foo {}
console.log(foo.toString()); // [object Object]
console.log(foo[Symbol.toStringTag]); // undefined
class Bar{
constructor(){
this[Symbol.toStringTag]='Bar'//设置symbol.toStringTag设置返回值
}
}
let bar = new Bar()
console.log(bar) //Bar()
console.log(bar.toString()) //[Object Bar]
console.log(bar[Symbol.toStringTag])//Bar
Symbol.unscopables
根据ECMAScript规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with环境绑定中排除”。设置这个符号并让其映射对应属性的键值为true,就可以阻止该属性出现在with环境绑定中,如下例所示:
1 let o = { foo: 'bar' };
2 with (o) {
3 console.log(foo); // bar
4 }
5 o[Symbol.unscopables] = {
6 foo: true
7 };
8 with (o) {
9 console.log(foo); // ReferenceError
10 }
注:不推荐使用with,因此也不推荐使用symbol.unscopables
with语句 扩展一个语句的作用域链。关于with的缺点不展开讲,主要是语义不明且造成性能损失 所以不推荐使用