本节将会重点分析ES6引入的第6种基本类型:Symbol(符号)。符号可以像字符串那样作为对象的属性名,只是它有唯一性的特点,可以避免属性名之间的冲突。
一、创建
符号没有字面量形式,只能通过Symbol()函数创建。该函数有一个可选的参数,只是用来描述当前符号,除了便于阅读之外,没有其他用途。由此可知,即使两个符号的描述相同,它们还是不能画等号。注意,Symbol()不是构造函数,因此不能和new运算符组合使用,否则会抛出类型错误。下面用一个例子展示符号的创建。
var sym1 = Symbol(), sym2 = Symbol("name"), sym3 = Symbol("name"), sym4 = new Symbol(); //抛出类型错误 console.log(sym2 === sym3); //false
如果要识别一个变量是否为符号,可以用typeof运算符。ES6扩展了它,当检测到符号时,能返回一个新的类型字符串“symbol”,具体如下所示。
typeof sym1; //"symbol" typeof sym2; //"symbol"
二、类型转换
符号在类型转换时表现得并不灵活,它无法与数字或字符串进行运算,也无法显式的转换成数字。如下所示,后面四条语句在执行时都会报错。
var sym = Symbol("age"); Number(sym); parseInt(sym); 1 + sym; "" + sym;
不过,符号可以显式的转换成字符串或布尔值,具体如下所示。
Boolean(sym); //true !sym; //false sym.toString(); //"Symbol(age)" String(sym); //"Symbol(age)"
三、全局共享
ES6会在内部维护一张全局符号注册表,通过Symbol.for()方法,可以登记指定符号,使其变成一个全局有效地符号,从而达到全局共享。该方法只接收一个参数,这个参数既是注册表中的键值,同时也是此符号的描述。下面的代码调用了两次Symbol.for()方法,传递了相同的参数,返回的却是同一个全局符号。
var sym1 = Symbol.for("name"), sym2 = Symbol.for("name"); console.log(sym1 === sym2); //true
在上面的代码中,第一次调用Symbol.for()方法时,会在注册表中搜索键值为“name”的符号,由于没有找到,所以就会创建一个新的符号。而在第二次调用Symbol.for()方法时,由于传递的键值与前一次相同,因此会返回刚刚的那个符号。从而可知,对变量sym1和sym2进行全等比较,返回的结果将是true。
如果要获取某个全局符号所对应的键值(即它的描述),那么可以通过Symbol.keyFor()实现,具体操作如下所示。
Symbol.keyFor(sym1); //"name" Symbol.keyFor(sym2); //"name"
四、属性名
本节开头曾提到过对象的属性名可以用符号表示,而这类属性可以有三种赋值方式。第一种是用方括号,注意,不能用点号,因为点号后面的标识符会被识别成字符串而不是符号。下面代码分别用方括号和点号为obj对象的sym属性赋值,前者被识别成了符号属性,而后者却被识别成了字符串属性。
var sym = Symbol("name"), obj = {}; obj[sym] = "strick"; obj.sym = "strick"; console.log(obj); //{Symbol(name): "strick", sym: "strick"}
第二种是在创建对象字面量时,用计算属性名的方式(即属性名被方括号包裹)为其赋值,如下所示。
obj = { [sym]: "freedom" };
第三种是调用Object.defineProperty()或Object.defineProperties()方法来为符号属性赋值,如下所示。
Object.defineProperty(obj, sym, { value: "justice" });
注意,符号属性是不可枚举的,既不能被for-in等循环遍历到,也不能被Object.keys()、Object.getOwnPropertyNames()等方法读取到。但可以通过Object.getOwnPropertySymbols()方法获得对象的符号属性,具体如下所示。
obj = { [sym]: "freedom", age: 28 }; Object.keys(obj); //["age"] Object.getOwnPropertyNames(obj); //["age"] Object.getOwnPropertySymbols(obj); //[Symbol(name)]
五、内置符号
ES6提供了一些内置符号,也叫做知名符号(Well-Known Symbol)。它们暴露了语言的内部逻辑,允许开发人员修改或拓展规范所描述的对象特征或行为。每一个内置符号对应Symbol对象的一个属性,例如Symbol.hasInstance、Symbol.iterator等,表1列出了11个内置符号。
表1 内置符号
属性名称 | 值类型 | 描述 |
hasInstance | 方法 | 当使用instanceof运算符时会调用该方法 |
isConcatSpreadable | 布尔值 | 当对象作为Array.prototype.concat()方法的参数时,控制该对象是否被展开 |
iterator | 方法 | 返回一个迭代器,用于定义一个可迭代的对象 |
match | 方法 | 当对象作为String.prototype.match()方法的参数时,该方法会被调用 |
replace | 方法 | 当对象作为String.prototype.replace()方法的参数时,该方法会被调用 |
search | 方法 | 当对象作为String.prototype.search()方法的参数时,该方法会被调用 |
split | 方法 | 当对象作为String.prototype.split()方法的参数时,该方法会被调用 |
species | 方法 | 创建派生类的构造函数 |
toPrimitive | 方法 | 当对象需要转换成原始值(即执行ToPrimitive抽象操作)时,该方法会被调用 |
toStringTag | 字符串 | 指定对象的类型,可在调用Object.prototype.toString()方法的时候返回 |
unscopables | 对象 | 保存在这个对象中的属性将不能被with语句所引用 |
下面会给出4个内置符号的示例,分别是hasInstance、isConcatSpreadable、match和toStringTag。
let digit = { [Symbol.hasInstance](number) { return !(number % 2); //判断数字是否为偶数 } }; 1 instanceof digit; //false 2 instanceof digit; //true let arr1 = [3, 4]; [1, 2].concat(arr1); //[1, 2, 3, 4] let arr2 = [3, 4]; arr2[Symbol.isConcatSpreadable] = false; //禁止展开 [1, 2].concat(arr2); //[1, 2, [3, 4]] let regex = { [Symbol.match](str) { return str.substr(0, 10); } }, message = "My name is strick"; message.match(regex); //"My name is" let people = { [Symbol.toStringTag]: "People" }; people.toString(); //"[object People]"