回忆一下JS中的原始类型:字符串型、数字型、布尔型、null和undefined。
ES6中引入了第6种原始类型:Symbol
创建Symbol
let firstName = Symbol();
let person = {};
person[firstName] = '欧阳不乖'
console.log(person[firstName]); //'欧阳不乖'
Symbol函数接受一个可选参数,可以添加一段文本描述即将创建的Symbol,这段属描述不可用于属性访问,但是建议每次创建Symbol时都添加一段描述,便于阅读代码和调试Symbol程序。
let firstName = Symbol('first name');
let person = {};
person[firstName] = '欧阳不乖';
console.log('first name' in person); //false
console.log(person[firstName]); // ''欧阳不乖
console.log(firstName); //Symbol('first name')
Symbol的描述被存储在内部的[[Description]]属性中,只有调用Symbol的toString()方法时才可以读取这个属性。在执行console.log的时候隐式的调用了toString()方法。
- Symbol的辨识方法
Symbol是原始值,且ES6同时扩展了typeof操作符,支持返回“Symbol”,所以可以用typeof来检测变量是否为Symbol类型
let symbol = Symbol('test symbol');
console.log(typeof symbol); //'symnbol'
Symbol的使用方法
所有使用可计算属性名的地方,都可以使用Symbol。
let firstName = Symbol('first name');
let person = {
//使用一个可计算对象字面量属性
[firstName] : '欧阳不乖'
}
//将属性设置为只读
Object.defineProperty( person, firstName, { writable : false});
console.log(person[firstName]); //'欧阳不乖'
Symbol共享体系
如果想创建一个可共享的Symbol,要使用Symbol.for()方法。它只接受一个参数,也就是即将创建的Symbol的字符串标识符,这个参数同样也被用作Symbol的描述:
let uid = Symbol.for('uid');
let object = {};
object[ uid ] = '12345';
console.log(object[uid]); //12345
console.log(uid); //Symbol(uid)
Symbol.for()方法首先在全局Symbol注册表中搜索键为‘uid’的Symbol是否存在,如果存在,直接返回已有的Symbol;否则,创建一个新的Symbol,并使用这个键在Symbol全局注册表中注册,随机返回新创建的Symbol。
后续如果再传入同样的键调用Symbol.for()会返回相同的Symbol:
let uid = Symbol.for('uid');
let uid2 = Symbol.for('uid');
let object = {
[uid] : '12345'
} ;
console.log(uid === uid2); //true
console.log(object[uid]); //12345
console.log(object[uid2]); //12345
还有一个与Symbol共享有关的特性:可以使用Symbol.keyFor()方法在Symbol全局注册表中检索与Symbol有关的键:
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
Symbol全局注册表是一个类似全局作用域的共享环境,也就是说你不能假设目前环境中存在哪些键
Symbol与类型强制转换
由于其他类型没有与Symbol逻辑等价的值,所以不能将Symbol强制转换为字符串或是数字类型。
在使用console.log()方法来输出Symbol的内容时,它会调用Symbol的String()方法并输出有用的信息。也可以像下面这样直接调用String()方法来获取相同的内容:
let uid = Symbol.for('uid'),
desc = String(uid);
console.log(desc); //Symbol(uid)
String()函数调用了uid.toString()方法,返回字符串类型的Symbol描述内容,但是,如果将Symbol与一个字符串拼接会导致程序抛出错误:
let uidDesc = Symbol.for('uid') + ''; //报错
Symbol不可以被转为字符串,同样也不能转为数字类型:
let uidSum = Symbol.for('uid') /1; //报错
只有在使用逻辑操作符的时候,Symbol可以正常运行,因为Symbol与JS中的非空值类似,其等价布尔值为true
Symbol属性检索
Object.keys()和Object.getOwnPropertyNames()方法可以检索对象中所有的属性名:前一个方法返回所有的可枚举属性名;后一个方法不考虑属性的可枚举性一律返回。在ES6中新增一个Object.getOwnPropertySymbols()方法来检索对象中的Symbol属性。
Object.getOwnPropertySymbols()方法的返回值是一个包含所有Symbol自有属性的数组:
let uid = Symbol.for('uid');
let object = {
[uid]:12345
}
let symbols = Object.getOwnPropertySymbols(object);
console.log(symbols.length); //1
console.log(symbols[0]); //Symbol('uid')
console.log(object[symbols[0]]); //12345
通过well-known Symbol暴露内部操作
-
Symbol.hasInstance:一个在执行instanceof时调用的内部方法,用来检测对象的继承信息。
Symbol.hasInstance方法只接受一个参数,即要检查的值。如果传入的值是函数的实例,则返回true:
let arr = []
console.log(arr instanceof Array); //true
// 等价于
console.log(Array[Symbol.hasInstance](arr)); //true
本质上,ES6只是将instanceof操作符重新定义为此方法的简写语法,现在引入方法调用以后,就可以随便改变instanceof的运行方式了:
function MyObject(){
// empty
}
Object.defineProperty(MyObject, Symbol.hasInstance,{
value : function(v){
//console.log(v); //MyObject {}
return false;
}
})
let obj = new MyObject();
console.log(obj instanceof MyObject ); // false
/*
* obj实际上是MyObject的实例,但是我们将Symbol.hasInstance的返回值硬编码为false以后
* 即使使用instanceof运算符也只是返回false
*/
我们可以按照自己的喜好任意重构Symbol.hasInstance,但是改写源码会造成不可预期的后果,所以请在必要的情况下只改写自己声明的函数Symbol.hasInstance属性
-
Symbol.isConcatSpreadable:一个布尔值,用于表示当传递一个集合作为Array.prototype.concat()方法的参数时,是否应该将合集内的元素规整到同一层级。
JS数组的concat方法被设计用于拼接两个数组,不但接受数组参数,也可以接收非数组参数:
let colors1 = ['red'];
let colors2 = ['green'];
let color3 = 'black';
console.log(colors1.concat(colors2, color3)); //["red", "green", "black"]
JS规范声明,凡是传入了数组的参数,就会自动将他们分解为独立元素。
Symbol.isConcatSpreadable属性是一个布尔值,如果该属性值为true,则表示对象有length属性和数字键,故它的数值型属性值应该被独立添加到concat调用的结果中。这个属性默认情况下不会出现在标准对象中,它只是可选属性,用于增强作用于特定对象类型的concat方法的功能,有效简化其默认特性:
let collection = {
0:'Hello',
1:'World',
length:2,
[Symbol.isConcatSpreadable]:true
}
let message = ['Hi'].concat(collection);
console.log(message.length); //3
console.log(message); //["Hi", "Hello", "World"]
// 假设将 [Symbol.isConcatSpreadable]:false改为这样,那么运行结果就变为了:
/ *
* console.log(message);
* ['Hi', {
* 0:'Hello',
* 1:'World',
* length:2,
* [Symbol.isConcatSpreadable]:true
* }]
* /
- Symbol.match:一个在调用String.prototype.match()方法时调用的方法,用于比较字符串。接受一个字符串类型的参数,如果匹配成功则返回匹配元素的数组,否则返回null。
- Symbol.replace:一个在调用String.prototype.replace()方法时调用的方法,用于替换字符串的子串。接受一个字符串类型的参数和一个替换用的字符串,最终依然返回一个字符串。
- Symbol.search:一个在调用String.prototype.search()方法时调用的方法,用于在字符串中定位子串。接受一个字符串类型的参数,如果匹配到则返回数字索引,否则返回 -1 。
-
Symbol.split:一个在调用String.prototype.split()方法时调用的方法,用于分割字符串。接受一个字符串参数,根据匹配内容将字符串分解,并返回一个包含分解后片段的数组。
在JS中字符串与正则表达式经常一起使用,尤其是字符串类型的几个方法,可以接受正则表达式作为参数:
1. match(regex) 确定给定字符串是否匹配正则表达式regex
2. replace(regex,replacement) 将字符串中匹配正则表达式的regex部分替换为replacement
3. search(regex) 在字符串中定位匹配正则表达式regex的位置索引
4. split(regex) 按照匹配正则表达式regex的元素将字符串分切,并将结果存入数组
在ES6之前,以上4个方法无法使用开发者自定义的对象来替代正则表达式进行字符串匹配。而在ES6中定义了与上边4个方法相对应的4个Symbol,将语言内建的RegExp对象的原生特性完全暴露出来。
// 实际上等价于 /^.{10}$/
let hasLengthOf10 = {
[Symbol.match]:function(value){
return value.length ===10 ? [ value ] : null ;
},
[Symbol.replace]:function(value, replacement){
return value.length ===10 ? replacement : value ;
},
[Symbol.search]:function(value){
return value.length ===10 ? 0 : -1 ;
},
[Symbol.split]:function(value){
return value.length ===10 ? [ , ] : [ value ] ;
},
};
let message1 = 'Hello world'; //11个字符
let message2 = 'Hello 1234'; //10个字符
console.log( message1.match(hasLengthOf10) ); //null
console.log( message2.match(hasLengthOf10) ); //["Hello 1234"]
console.log( message1.replace(hasLengthOf10,'欧阳不乖') ); //Hello world
console.log( message2.replace(hasLengthOf10,'欧阳不乖') ); //欧阳不乖
console.log( message1.search(hasLengthOf10) ); // -1
console.log( message2.search(hasLengthOf10) ); // 0
console.log( message1.split(hasLengthOf10) ); //["Hello world"]
console.log( message2.split(hasLengthOf10) ); // [ , ] 注意这里原书写的['','']个人认为二者有区别
-
Symbol.toPrimitive:该方法被定义在每一个标准类型的原型上,并且规定了当对象被转换为原始值时应该执行的操作。
该方法接受一个值作为参数,该值在规范中被称为“类型提示(hint)”,分别是:number、string或default,对应的返回分别是数字、字符串活无类型偏好的值。
对于大多数标准对象,数字模式有以下的特性,根据优先级的顺序排列如下:
1.调用valueOf()方法,结果为原始值,则返回;
2.否则调用toString()方法,结果为原始值,则返回;
3.如果再无可选值,则抛出错误。
对于大多数标准对象,字符串模式有以下优先级排序:
1.调用toString()方法,结果为原始值,则返回;
2.否则调用valueOf()方法,结果为原始值,则返回;
3.如果再无可选值,则抛出错误。
在大多数情况下,标准对象会将默认模式按数字模式处理(除了Date对象,在这种情况下,会将默认模式按字符串模式处理)
function Temperature(degrees){
this.degrees = degrees;
}
Temperature.prototype[Symbol.toPrimitive] = function(hint){
switch (hint){
case 'string' :
return this.degrees + '\u00b0' ; //degrees symbol
case 'number':
return this.degrees;
case 'default':
return this.degrees + '度'
}
}
var freezing = new Temperature(32);
console.log( freezing/2 ) //16
console.log(String(freezing) ) //32°
console.log( freezing + '!' ); //32度!
- Symbol.toStringTag:一个在调用Object.prototype.toString()方法时使用的字符串,用于创建对象描述。
- Symbol.unscopables:一个定义了一些不可被with语句引用的对象属性名称的对象集合。
Symbol 的一些小扩展
let firstName = Symbol('欧阳不乖');
let lastName ='Hello';
let person = {
[firstName]:'爱谁谁',
[lastName]:'World'
}
console.log( person.firstName ); // undefined
console.log( person[Symbol('欧阳不乖')] ) //undefined
console.log( person[firstName] ); // 爱谁谁
console.log( person.last ); // undefined
console.log( person[lastName] ); // World
console.log( firstName ); // Symbol('欧阳不乖')
console.log( Symbol('欧阳不乖') ); // Symbol('欧阳不乖')
console.log( firstName==Symbol('欧阳不乖') ); // false
console.log( String(Symbol('欧阳不乖'))+'yes' ); //Symbol(欧阳不乖)yes
console.log( Symbol('欧阳不乖')=== Symbol('欧阳不乖') ); //false
console.log( Symbol('欧阳不乖')== Symbol('欧阳不乖') ); //false
console.log( Object.is( Symbol('欧阳不乖'), Symbol('欧阳不乖')) ); //false