一、Symbol简介
ECMAScript 6引入了一种新的原始类型:Symbol,它的功能类似于一种标识唯一性的ID。
所有原始值,除了Symbol以外都有各自的字面形式,例如布尔类型的true或数字类型的42。
二、创建Symbol
可以通过全局的Symbol函数创建一个Symbol:
let firstName = Symbol("first name");
let person = {};
person[firstName] = "Nicholas";
console.log(person[firstName]); // "Nicholas"
由于Symbol是原始类型,因此调用new Symbol()会导致程序抛出错误。
Symbol函数接受一个可选参数,以添加一段文本描述即将创建的Symbol,建议每次创建Symbol时都添加这样一段描述,以便于阅读代码和调试Symbol程序:
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(firstName.toString()); // "Symbol(first name)"
Symbol的描述被存储在内部的[[Description]]属性中,只有当调用Symbol的toString()方法时才可以读取这个属性。
注意:Symbol是原始值,且ECMAScript6同时扩展了typeof操作符,支持返回"Symbol",所以可以用typeof来检测变量是否为Symbol类型:
let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"
三、Symbol共享体系
有时我们可能希望在不同的对象中共享同一个Symbol,一般而言,在很大的代码库中或跨文件追踪Symbol非常困难而且容易出错,出于这些原因,ECMAScript6提供了一个可以随时访问的全局Symbol注册表。
如果想创建一个可共享的Symbol,要使用Symbol.for()方法。
它只接受一个参数,也就是即将创建的Symbol的字符串标识符,这个参数同样也被用作Symbol的描述:
let uid = Symbol.for('uid');
let object = {
[uid]: "12345"
};
console.log(object[uid]); // "12345"
console.log(uid); // "Symbol(uid)"
let uid2 = Symbol.for('uid');
console.log(uid == uid2); // true
console.log(object[uid2]); // "12345"
console.log(uid2); // "Symbol(uid)"
Symbol.for()方法首先在全局Symbol注册表中搜索键为"uid"的Symbol是否存在,如果存在,直接返回已有的Symbol;否则创建一个新的Symbol,并使用这个键在Symbol全局注册表中注册,随即返回新创建的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全局注册表中不存在uid3这个Symbol,也就是不存在与之有关的键,所以最终返回undefined。
Symbol全局注册表是一个类似全局作用域的共享环境,也就是说不能假设目前环境中存在哪些键。
当使用第三方组件时,尽量使用Symbol键的命名空间以减少命名冲突。
四、Symbol与类型强制转换
其他类型没有与Symbol逻辑等价的值,因而Symbol使用起来不是很灵活,尤其是不能将Symbol强制转换为字符串和数字类型,否则如果不小心将其作为对象属性,最终会导致不一样的执行结果。
let uid = Symbol.for('uid');
desc = uid + ""; // 报错!
let uid2 = Symbol.for('uid');
sum = uid2 / 1; // 报错!
Symbol与一个字符串拼接,会导致程序抛出错误,也不能将Symbol强制转换为数字类型。
五、Symbol属性检索
ES6添加了一个Object.getOwnPropertySymbols()方法来检索对象中的Symbol属性,它的返回值是一个包含所有Symbol自有属性的数组:
let uid = Symbol.for('uid');
let object = {
[uid]: '12345',
abc: '1234213412'
};
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暴露内部操作
ECMAScript6主要通过在原型链上定义与Symbol相关的属性来暴露更多的语言内部逻辑。
ECMAScript开放了以前JavaScript中常见的内部操作,并通过预定义一些well-known Symbol来表示。每一个这类Symbol都是Symbol对象的一个属性。
6.1、Symbol.hasInstance方法
每一个函数中都有一个Symbol.hasInstance方法,用于确定对象是否为函数的实例。
该方法在Function.prototype中定义,所以所有函数都继承了instanceof属性的默认行为。
为了确保Symbol.hasInstance不会被意外重写,该方法被定义为不可写、不可配置并且不可枚举。
Symbol.hasInstance方法只能接受一个参数,即要检查的值。如果传入的值是函数的实例,则返回true:
obj instanceof Array;
以上这行代码等价于下面这行:
Array[Symbol.hasInstance](obj);
现在使用Symbol.hasInstance方法就可以随意改变instanceof的运行方式了。
假设想定义一个无实例的函数,就可以将Symbol.hasInstance的返回值硬编码为false:
function MyObject(v){
}
Object.defineProperty(MyObject, Symbol.hasInstance, {
value: function(v){
return false;
}
});
let obj = new MyObject();
console.log(obj instanceof MyObject);
也可以基于任意条件,通过值检查来确定被检测的是否为实例。
例如可以将1~100的数字定义为一个特殊数字类型的实例:
function SpecialNumber(){
// 空函数
}
Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
value: function(v){
return (v instanceof Number) && (v >= 1 && v <= 100);
}
});
var two = new Number(2),
zero = new Number(0);
console.log(two instanceof SpecialNumber); // true
console.log(zero instanceof SpecialNumber); // false
6.2、Symbol.isConcatSpreadable属性
JavaScript数组的concat()方法被设计用于拼接两个数组:
let colors1 = ["red", "green"],
colors2 = colors1.concat(["blue", "black"]),
colors3 = colors1.concat(["blue", "black"], "brown");
console.log(colors2.length); // 4
console.log(colors2); // ["red", "green", "blue", "black"]
console.log(colors3.length); // 5
console.log(colors3); // ["red", "green", "blue", "black", "brown"]
JavaScript规范声明,凡是传入了数组参数,就会自动将它们分解为独立元素。在ECMAScript6标准以前,我们根本无法调整这个特性。
Symbol.isConcatSpreadable属性是一个布尔值,如果该属性值为true,则表示对象有length属性和数字键,故它的数值型属性值应该被独立添加到concat()调用的结果中。
它与其他well-known Symbol不同的是,这个Symbol属性默认情况下不会出现在标准对象中,它只是一个可选属性,用于增强作用于特定对象类型的concat()方法的功能,有效简化其默认特性。
可以通过以下方法,定义一个在concat()调用中与数组行为相近的新类型:
let collection = {
0: 'Hello',
1: 'world',
length: 2,
[Symbol.isConcatSpreadable]: true
};
let messages = ["Hi"].concat(collection); // 将collection中的属性分解为独立元素
console.log(messages.length);
console.log(messages); // ["Hi", "Hello", "world"]
以上示例中,定义了一个类数组对象collection:它有一个length属性,还有两个数字键,Symbol.isConcatSpreadable属性值为true表明属性值应当作为独立元素添加到数组中。
6.4、Symbol.toPrimitive方法
在JavaScript引擎中,当执行特定操作时,经常会尝试将对象转换到相应的原始值,例如,比较一个字符串和对象,如果使用双等号(==)运算符,对象会在比较操作执行前被转换为一个原始值。
到底使用哪一个原始值以前是由内部操作决定的,但在ES6的标准中,通过Symbol.toPrimitive方法可以更改那个暴露出来的值。
Symbol.toPrimitive方法被定义在每一个标准类型的原型上,并且规定了当对象被转换为原始值时应当执行的操作。每当执行原始值转换时,总会调用Symbol.toPriitive方法并传入一个值作为参数,这个值在规范中被称作类型提示(hint)。类型提示参数的值只有三种选择:
- "number"
- "string"
- "default"
传递这些参数时,Symbol.toPrimitive返回的分别是:数字、字符串或无类型偏好的值。
对于大多数标准对象,数字模式有以下特性,根据优先级的顺序排列如下:
1、调用valueOf()方法,如果结果为原始值,则返回
2、否则,调用toString()方法,如果结果为原始值,则返回
3、如果再无可选值,则抛出错误
同样,对于大多数标准对象,字符串模式有以下优先级排序:
1、调用toString()方法,如果结果为原始值,则返回
2、否则,调用valueOf()方法,如果结果为原始值,则返回
3、如果再无可选值,则抛出错误
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 + " degrees";
}
};
var freezing = new Temperature(32);
console.log(freezing + "!"); // "32 degrees!"
console.log(freezing / 2); // 16
console.log(String(freezing)); // "32°"
每一条console.log()语句将触发不同的hint参数值:
- +运算符触发默认模式
- /运算符触发数字模式
- String()函数触发字符串模式
6.5、Symbol.toStringTag属性
ES6之前面对存在多个全局执行环境,数组类型识别这样的问题时,通常采用这样的解决方案:
function isArray(value){
return Object.prototype.toString.call(value) === "[object Array]";
}
console.log(isArray([])); // true
ES6通过Symbol.toStringTag这个Symbol改变了调用Object.prototype.toString()时返回的身份标识。
这个Symbol所代表的属性在每一个对象中都存在,其定义了调用对象的Object.prototype.toString.call()方法时返回的值。
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的返回值。
然而,仍然可以定义自己的toString()方法,这不会影响Object.prototype.toString.call()方法的使用,但却可以提供一个不同的值:
function Person(name){
this.name = name;
}
Person.prototype[Symbol.toStringTag] = "Person";
Person.prototype.toString = function(){
return this.name;
}
var me = new Person('Nicholas');
console.log(me.toString()); // "Nicholas"
console.log(Object.prototype.toString.call(me)); // "[object Person]"
由于Person不再继承自Object.prototype.toString()方法,因而调用me.toString()方法时返回的是一个不同的值。
注意:除非另有说明,所有对象都会从Object.prototype继承Symbol.toStringTag这个属性,且默认的属性值为"Object"。