Symbol

Symbol和Symbol属性

Symbol是ES6引入的第6种原始类型数据。ES6之前,属性名都是字符串类型,Symbol可以为属性添加非字符串名称。

创建

所有原始值,除了Symbol以外都有各自的字面量形式。通过全局的Symbol函数来创建一个SymbolSymbol函数接受一个可选参数,用来描述即将创建的Symbol,这段描述不可用于属性访问。

1
2
3
4
5
6
7
let firstName = Symbol('first name');
let person = {};
person[firstName] = 'Nicholas';
console.log('first name' in person); //false
console.log(person[firstName]); //'Nicholas'
console.log(firstName); //'Symbol(first name)'
console.log(typeof firstName); //'symbol'

不能通过new调用Symbol函数,这样会导致程序错误。
ES6扩展了typeof操作符,支持检测Symbol类型。
Symbol的描述被存储在内部的[[Description]]属性中,只有调用SymboltoString方法时才可以读取这个属性。执行console.log时,隐式调用了firstNametoString方法。

使用

所有使用可计算属性名的地方,都可以使用Symbol

1
2
3
4
5
6
7
8
9
10
11
12
13
let firstName = Symbol('first name');
//使用一个可计算对象字面量属性
let person = {
[firstName]: 'Nicholas'
}
Object.defineProperty(person, firstName, {writable: false});
let lastName = Symbol('last name');
Object.defineProperties(person, {
[lastName]: {
value: 'Zakas',
writable: false
}
})

Symbol共享体系

有时可能需要在不同的代码中共享同一个Symbol,使用Symbol.for()方法,创建一个共享的Symbol。该方法接受一个参数,也就是即将创建的Symbol的字符串标识符,这个参数也作为Symbol的描述。

1
2
3
4
5
6
7
8
9
10
let uid = Symbol.for('uid');
let obj = {};
obj[uid] = '123';
let uid2 = Symbol.for('uid');
console.log(uid === uid2); //true
console.log(obj[uid2]); //123
console.log(uid2); //'Symbol(uid)'

Symbol.for()方法首先在全局Symbol注册表中搜索键为uidSymbol是否存在,如果存在,返回已有的Symbol。否则,创建一个新的Symbol,并使用这个键在Symbol全局注册表中注册,随即返回新的Symbol

可以使用Symbol.keyFor()方法在Symbol全局注册表中检索与Symbol有关的键。

1
2
3
4
5
6
7
8
let uid = Symbol.for('uid');
console.log(Symbol.keyFor(uid)); //'uid'
let uid2 = Symbol.for('uid');
console.log(Symbol.keyFor(uid2)); //'uid'
let uid3 = Symbol('uid');
console.log(Symbol.keyFor(uid3)); //undefined

uiduid2都返回"uid"这个键,而在全局注册表中不存在uid3这个Symbol,也就不存在与之有关的键,所以返回undefined

Symbol与类型强制转换

JavaScript中,某些情况下,会发生自动转换类型行为。然而,其他类型没有与Symbol逻辑等价的值,尤其是不能将Symbol强制转换成字符串和布尔值

1
2
3
4
5
6
7
let uid = Symbol.for('uid'),
desc = String(uid);
console.log(desc);
let desc2 = uid + ''; //TypeError: Cannot convert a Symbol value to a string
let sum = uid/1; //TypeError: Cannot convert a Symbol value to a number

String()函数调用了uid.toString方法,返回字符串类型的Symbol描述里的内容。但是将Symbol强制转换成字符串或数字类型,会抛出错误。

Symbol属性检索

Object.keys()Object.getOwnPropertyNames()方法可以检索对象中的属性名,前一个返回所有可枚举属性,后一个不考虑是否可枚举,一律返回。这两个方法都不支持Symbol属性。ES6中新增一个Object.getOwnPropertySymbols()方法检索对象中的Symbol属性,该方法返回一个包含所有Symbol自有属性(非继承)的数组。

1
2
3
4
5
6
let uid = Symbol.for('uid');
let obj = {
[uid]: 123
}
let symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); //[ Symbol(uid) ]

通过well-known Symbol暴露内部操作

ES6开放了以前JavaScript中常用的内部操作,并通过预定义一些well-known Symbol来表示。每一个这类Symbol都是Symbol对象的一个属性。这些well-known Symbol包括

  • Symbol.hasInstance一个在执行instanceof时调用的内部方法,用于检测对象的继承信息。
  • Symbol.isConcatSpreadable一个布尔值,用于表示当传递一个集合作为Array.prototype.concat()方法的参数时,是否应该将集合内的元素归整到同一层级。
  • Symbol.iterator一个返回迭代器方法
  • Symbol.matchSymbol.replaceSymbol.searchSymbol.split分别在调用String.prototype.match()String.prototype.replace()String.prototype.search()String.prototype.split()方法时调用的方法
  • Symbol.species用于创建派生类的构造函数。
  • Symbol.toPrimitive一个返回对象原始值的方法
  • Symbol.toStringTag一个在调用Object.prototype.toString()方法时使用的字符串,用于创建对象描述。
  • Symbol.unscopables一个定义了一些不可被with语句引用的对象属性名称的对象集合。

Symbol.hasInstance

每一个函数都有一个Symbol.hasInstance方法,用于确定对象是否是函数的实例。该方法在Function.prototye中定义,所有函数都继承了instanceof属性的默认行为。该方法不可写,不可配置并且不可枚举。

Symbol.hasInstance方法只接受一个参数,即要检查的值。如果传入的值是函数的实例,则返回true

1
2
3
4
5
6
function myObject(){
//
}
let obj = new myObject();
obj instanceof myObject; //true
myObject[Symbol.hasInstance](obj); //true

本质上,ES6只是将instanceof操作符重新定义为此方法的简写语法。现在引入调用后,就可以随意改变instanceof的运行方式了。

1
2
3
4
5
6
7
8
9
10
11
function myObject(){
//
}
//给myObject定义一个Symbol.hasInstance用来屏蔽继承
Object.defineProperty(myObject, Symbol.hasInstance, {
value: function(v){
return false
}
})
let obj = new myObject();
console.log(obj instanceof myObject); //false

objmyObject的实例,因为给myObject改写Symbol.hasInstance,为其定义一个总是返回false的新函数。

Symbol.isConcatSpreadable

Symbol.isConcatSpreadable属性是一个布尔值,如果该属性值为true,则表示对象有length属性和数字键,它的数值型属性值应该被独立添加到concat()调用结果中。它与其他well-known Symbol不同的是,这个Symbol属性默认情况下不会出现在标准对象中,它是一个可选属性,用于增强作用于特定对象类型的concat()方法的功能,有效简化其默认特性。

1
2
3
4
5
6
7
8
9
10
let collection = {
0: 'hello',
1: 'world',
a: 12,
length: 2,
[Symbol.isConcatSpreadable]: true
}
let messages = ['Hi'].concat(collection);
console.log(messages.length); //3
console.log(messages); //[ 'Hi', 'hello', 'world' ]

也可以在派生数组子类中将Symbol.isConcatSpreadable设置为false,从而防止元素在调用concat()方法时被分解。

Symbol.match、Symbol.replace、Symbol.search、Symbol.split属性

字符串方法matchreplacesearchsplit可以接受正则表达式作为参数,在ES6之前,无法使用开发者自定义的对象来替代正则表达式进行字符串匹配。在ES6中,定义了与上述方法对应的Symbol,这4个Symbol属性表示对应字符串方法的第一个参数应该调用的正则表达式参数的方法,它们定义在RegExp.prototype中,是字符串方法应该使用的默认是想。

  • Symbol.match接受一个字符串类型的参数,如果匹配成功,返回匹配元素的数组,否则返回null
  • Symbol.replace接受一个字符串类型的参数和一个替换用的字符串,最终返回一个字符串
  • Symbol.search接受一个字符串参数,如果匹配到内容,则返回数字类型的索引位置,否则返回-1
  • Symbol.split接受一个字符串参数,根据匹配内容将字符串分解,并返回一个包含分解后片段的数组

如果在对象中定义这些属性,即使不使用正则表达式和以正则表达式为参的方法也可以在对象中实现模式匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let hasLengthOf10 = {
[Symbol.match]: function(value){
return value.length === 10? [value.substring(0,10)]: null
},
[Symbol.replace]: function(value, replacement){
return value.length === 10? value: replacement + value.substring(10)
},
[Symbol.search]: function(value){
return value.length === 10? 0: -1
},
[Symbol.split]: function(value){
return value.length === 10? ["", ""]: [value]
}
}
let msg1 = 'Hello world',
msg2 = 'Hello John';
let match1 = msg1.match(hasLengthOf10),
match2 = msg2.match(hasLengthOf10),
replace1 = msg1.replace(hasLengthOf10, 'hey'),
replace2 = msg2.replace(hasLengthOf10, 'hey'),
search1 = msg1.search(hasLengthOf10),
search2 = msg2.search(hasLengthOf10),
split1 = msg1.split(hasLengthOf10),
split2 = msg2.split(hasLengthOf10);
console.log(match1, match2); //null [ 'Hello John' ]
console.log(replace1, replace2); //'heyd' 'Hello John'
console.log(search1, search2); //-1 0
console.log(split1, split2); //[ 'Hello world' ] [ '', '' ]

尽管hasLengthOf10不是正则表达式,但给它添加了相应的Symbol属性,所以传递给字符串方法,可以正常运行。

Symbol.toPrimitive方法

再JavaScript引擎中,当执行特定操作时,会尝试将对象转换到相应的原始值。到底使用哪一个原始值以前是由内部操作决定的。在ES6中,通过Symbol.toPrimitive方法可以更改返回的原始值。

Symbol.toPrimitive方法被定义在每一个标准类型的原型上,并且规定了当对象被转换为原始值应当执行的操作。当执行原始值转换时,总是会调用Symbol.toPrimitive方法并传入一个值做为参数,这个值在规范中被称作类型提示(hint)。该值有3个选择:"number""string""default",对应这些参数,Symbol.toPrimitive返回的分别是:数字、字符串、无类型偏好的值。

Symbol.toPrimitive方法参数传入的为"number",大多数标准对象,有以下特性。

  • 调用valueOf方法,如果结果是原始值,则返回
  • 否则,调用toString方法,如果结果是原始值,则返回
  • 如果无再可选值,则抛出错误

Symbol.toPrimitive方法参数传入的为"string",大多数标准对象,有以下特性。

  • 调用toString方法,如果结果是原始值,则返回
  • 否则,调用valueOf方法,如果结果是原始值,则返回
  • 如果无再可选值,则抛出错误

大多数情况下,标准对象会将默认模式按数字("number")模式处理(Date对象,默认模式是字符模式)。可以自定义Symbol.toPrimitive方法,覆盖这些默认的强制转换特性。

默认模式只用于==+运算以及Date构造函数传递一个参数时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Temperature(degrees){
this.degrees = degrees
}
Temperature.prototype[Symbol.toPrimitive] = function(hint){
switch(hint){
case 'string':
return this.degrees + '\u00b0';
case 'number':
return this.degrees;
case 'default':
return this.degrees + ' degress'
}
}
var freezing = new Temperature(32);
console.log(freezing + '!'); //32 degress!
console.log(freezing / 2); //16
console.log(String(freezing)); //32°

这里在构造函数Temperature.prototype上定义了Symbol.toPrimitive屏蔽了继承的Symbol.toPrimitive方法。新的方法根据hint指定的模式返回不同的值(hint有JavaScript引擎传入)。

Symbol.toStringTag

ES6中,通过Symbol.toStringTag改变调用Object.prototype.toString时返回的身份标识。这个Symbol所代表的属性在每一个对象中都存在,其定义了调用对象的Object.prototype.toString.call()方法时返回的值。

1
2
3
4
5
6
7
8
function Person(name){
this.name = name;
}
Person.prototype[Symbol.toStringTag] = 'Person';
var me = new Person('Nicholas');
console.log(me.toString()); //'[object Person]'
console.log(Object.prototype.toString.call(me)); //'[object Person]'

Person.prototype继承了Object.prototype.toString()方法,所以调用me.toString()方法时也使用了Symbol.toStringTag的返回值。

Symbol.unscopables

with语句设计的初衷是免于编写重复代码,但由此会带来,代码可读性变差,执行性能差且容易导致程序出错。最终标准规定,严格模式下,不可以使用with语句。且这条限制同样影响到了类和模块,默认使用严格模式且没有任何退出的方法。

1
2
3
4
5
6
7
8
var values = [3,4,5],
colors = ['red', 'green', 'blue'],
color = 'black';
with(colors){
push(color);
push(...values)
}

在ES6环境中,with语句引用的values不是with语句外的变量values,而是数组本身的values方法,这样就脱离了代码原本的目标。(测试还是使用with语句外的变量,Node v10.0.0)。

ES6增加Symbol.unscopables,这个Symbol通常用于Array.prototype,已在with语句中标示出不创建绑定的属性名。Symbol.unscopables是以对象的形式出现,它的键是在with语句中要忽略的标识符,对应的值必须为true

1
2
3
4
5
6
7
8
9
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
keys: true,
values: true
})

这里在Array.prototypeSymbol.unscopables定义了ES6新的数组方法,这样在with语句中不再创建这些方法的绑定。