在 JS 已有的基本类型( 字符串、数值、布尔、 null 与 undefined ) 之外, ES6 引入了一种新的基本类型:符号(Symbol ) 。

符号起初被设计用于创建对象私有成员,而这也是 JS 开发者期待已久的特性。在符号诞生之前,将字符串作为属性名称导致属性可以被轻易访问,无论使用何种命名规则。而“私有名称”意味着开发者可以创建非字符串类型的属性名称,由此可以防止使用常规手段来探查这些名称。

“私有名称”提案最终发展成为 ES6 中的符号,而本章将会教你如何有效使用它。虽然它只保留了实现细节( 即:引入了非字符串类型的属性名) 而丢弃了私有性意图,但它仍然显著有别于对象的其余属性。

创建符号值

符号没有字面量形式,这在 JS 的基本类型中是独一无二的,有别于布尔类型的 true 或数值类型的 42 等等。你可以使用全局 Symbol 函数来创建一个符号值,正如下面这个例子:

1
2
3
4
let firstName = Symbol();
let person = {};
person[firstName] = "Nicholas";
console.log(person[firstName]); // "Nicholas"

此代码创建了一个符号类型的 firstName 变量,并将它作为 person 对象的一个属性,而每次访问该属性都要使用这个符号值。为符号变量适当命名是个好主意,这样便能轻易说明它的用意。

由于符号值是基本类型的值,因此调用 new Symbol() 将会抛出错误。你可以通过 new Object(yourSymbol) 来创建一个符号实例,但尚不清楚这能有什么作用。

Symbol 函数还可以接受一个额外的参数用于描述符号值,该描述并不能用来访问对应属性,但它能用于调试,例如:

1
2
3
4
5
6
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)"

符号的描述信息被存储在内部属性 [[Description]] 中,当符号的 toString() 方法被显式或隐式调用时,该属性都会被读取。在本例中, console.log() 隐式调用了 firstName 变量的 toString() 方法,于是描述信息就被输出到日志。此外没有任何办法可以从代码中直接访问 [[Description]] 属性。我建议始终应给符号提供描述信息,以便更好地阅读代码或进行调试。

由于符号是基本类型的值,因此你可以使用 typeof 运算符来判断一个变量是否为符号。 ES6 扩充了 typeof 的功能以便让它能返回 “symbol” ,例如:

1
2
let symbol = Symbol("test symbol");
console.log(typeof symbol); // "symbol"

尽管有其他办法可以判断一个变量是否为符号, typeof 运算符依然是最准确、最优先的判别手段。

使用符号值

你可以在任意能使用“可计算属性名”的场合使用符号。此前的例子已经展示了符号的方括号用法,而在对象的“可计算字面量属性名”中也能使用符号,还能在 Object.defineProperty() 或 Object.defineProperties() 调用中使用它,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}
});
console.log(person[firstName]); // "Nicholas"
console.log(person[lastName]); // "Zakas

这个例子首先使用对象的“可计算字面量属性”创建了一个符号类型的属性 firstName ,该属性是可枚举的( 译注:后面会提到与普通属性的差异) 。下一行代码将该属性设置为只读。接下来,使用 Object.defineProperties() 方法创建了一个只读的符号类型属性 lastName ,而此时再次使用了“可计算字面量属性”方式,并将其加入第二个调用参数。

既然能在任意可使用“可计算属性名”的场合使用符号,你就需要一种在不同代码段中共享符号值的体系,以便更有效地使用它们。