1 概述
首先来看ES6引入Symbol类型的原因:ES5的对象属性名都是字符串,这容易造成属性名的冲突。例如,我们使用他人提供的对象,在位这个对象添加新方法时方法名有可能与现有方法产生冲突,而在引入Symbol类型后,原来的属性名为字符串类型,新增的则为Symbol类型,这样自然就不冲突了,这对于一个对象由多个模块构成的情况非常有用。
Symbol值通过Symbol函数生成。
let s = Symbol();
typeof s //"symbol"
2 Symbol的参数使用
Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,作用:为了在控制台显示,或转为字符串时可以区分。
当Symbol的参数为一个对象时,则默认会调用对象的
toString
方法。
let s1 = Symbol('description');
s1 // Symbol(description)
s1.toString() // "Symbol(description)"
const sym=Symbol({});
sym // Symbol([object Object])
//重写toString方法
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)
由于参数只是表示对Symbol值的描述,相同参数的Symbol函数的返回值是不同的。也可以说Symbol值是独一无二的。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
我们可以借助Symbol.prototype.description属性来获取Symbol的描述:
const sym = Symbol('description');
sym.description // "description"
3 作为属性名的Symbol值
本文开头也说了一个属性名冲突的问题,那么具体如何来使用Symbol值作为对象的属性名呢?
注意:方式1不能使用点运算符(使用点运算符,会将属性名当作字符串类型),方式2必须加方括号(或叫中括号)。
let mySymbol = Symbol();
//方式1
let a= {};
a[mySymbol] = 666;
//方式2
let a = {
[mySymbol]: 666
};
//方式3
let a = {};
Object.defineProperty(a, mySymbol, { value: 666 });
当属性值为函数时:
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
//匿名函数可以采用增强的对象写法
let obj = {
[s](arg) { ... }
};
//调用函数
obj[s](参数);
注:Symbol值作为属性名时,则该属性为公开属性,不是私有属性。
Symbol作为属性名时,通过for...in
、for...of
循环,Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
方法都无法返回该属性。这时可以借助Object.getOwnPropertySymbols()
方法来获取,该方法返回的是一个数组,可以获取所有Symbol属性名。
另外,Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和Symbol键名。
4 Symbol类型转换
Symbol值可以转换为字符串和布尔值,但不能转为数值。
//转为字符串:有两种方式
let sym = Symbol('My symbol');
String(sym) // "Symbol(My symbol)"
sym.toString() // "Symbol(My symbol)"
//转为布尔值
let sym = Symbol();
Boolean(sym) // true
!sym // false
5 Symbol.for()与Symbol.keyFor()
我们直接举个例子吧:
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for()
方法接受一个字符串作为参数,然后搜索是否存在以该参数作为名称的Symbol值,若存在,则直接返回这个Symbol值,否则就会新建一个以该字符串为名称的Symbol值,即多次调用Symbol.for("字符串")
,返回的是同一个Symbol值,而利用Symbol()
方法每次都会生成新的Symbol值,多次调用则会生成不同的Symbol值。
另外,两者还有一个区别,Symbol.for()
生成的Symbol值会被等级在全局环境中供后续搜索重复使用,而Symbol()
写法没有登记机制,所以每次调用都会返回一个不同的值。
进一步地,Symbol.keyFor()
方法用于返回一个已登记的Symbol类型值的key。该方法也佐证了Symbol.for()
方法生成的值会被登记至全局环境中。
注:
Symbol.for()
为Symbol值登记的名字,是全局环境的,不管是否是在全局环境运行的。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
Symbol的应用示例-消除魔术字符串
魔术字符串指的是在代码中多次出现,与代码形成强耦合的某一个数值或字符串。下面来看一个例子:根据形状来计算面积。
function getArea(shape, options) {
let area = 0;
switch (shape) {
case 'Triangle': // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 });
分析:上面的例子中Triangle
就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
解决方法:将魔术字符串定义为一个变量,下次再修改时,直接去定义地方进行修改即可,不用再去代码中挨个修改。
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
进一步地,我们可以将shapeType.triangle
的值设置为一个Symbol:
const shapeType = {
triangle: Symbol('Triangle')
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
分析:可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。