正文
虽然ES2015已经引入了许多开发人员期待已久的语言特性,但还有一些新特性不太为人所知和理解,其好处也不太清楚——比如symbols。
symbol(符号)是一种新的原始数据类型,一个确保不会和其它符号冲突的唯一令牌。从这个意义上讲,你可以把符号看作是一种
UUID
(通用唯一识别码)。 让我们看看符号是如何工作的,以及我们能用它做些什么。
创建符号
创建符号非常直接,就是简单地调用
Symbol
函数。需要注意的是这只是一个标准的函数而不是一个对象构造器。使用new操作符调用它会导致一个类型错误。每次调用Symbol函数的时候,都会得到一个全新的唯一的值。
const foo = Symbol();
const bar = Symbol();
foo === bar
// <-- false
创建符号的时候可以给符号添加一个标签,方式是传递字符串作为第一个参数。标签并不能影响符号的值,只是利于调试,并且当符号的
toString()
方法被调用的时候会显示出来。创建具有相同标签的多个符号是可行的,但是这样做没有任何好处,并很可能引起疑惑。
let foo = Symbol('baz');
let bar = Symbol('baz');
foo === bar
// <-- false
console.log(foo);
// <-- Symbol(baz)
符号能用来做什么?
符号可以很好地替代作为类/模块常量的字符串或者整数:
class Application {
constructor(mode) {
switch (mode) {
case Application.DEV:
// Set up app for development environment
break;
case Application.PROD:
// Set up app for production environment
break;
case default:
throw new Error('Invalid application mode: ' + mode);
}
}
}
Application.DEV = Symbol('dev');
Application.PROD = Symbol('prod');
// Example use
const app = new Application(Application.DEV);
字符串和整数并不是唯一的值;比如数字
2
或者字符串
development
也可能在程序的其它地方被用于不同的目的。使用符号意味着我们可以对提供的值更有信心。
符号的另外一个有趣的用法是作为对象属性的键值。如果你曾经把JavaScript对象作为
hashmap
(PHP术语中的关联数组或者Python中的字典)使用,你就会对使用括号语法获取/设置属性很熟悉:
const data = [];
data['name'] = 'Ted Mosby';
data['nickname'] = 'Teddy Westside';
data['city'] = 'New York';
使用括号语法,我们也可以使用符号做为属性的键值。这样做有很多优点。首先,可以确保基于符号的键值不会冲突,不像字符串键值有可能会和对象已有的属性或者方法冲突。其次,符号不会被
for ... in
枚举,并且会被
Object.keys()
,
Object.getOwnPropertyNames()
,
JSON.stringify()
等方法忽略。对于在序列化对象时不想被包含的属性来说,符号是一个理想的选择。
const user = {};
const email = Symbol();
user.name = 'Fred';
user.age = 30;
user[email] = '[email protected]';
Object.keys(user);
// <-- Array [ "name", "age" ]
Object.getOwnPropertyNames(user);
// <-- Array [ "name", "age" ]
JSON.stringify(user);
// <-- "{"name":"Fred","age":30}"
然而,值得注意的是,使用符号做为键值并不能保证私有。有一些新的工具允许访问符号类型的属性键值。
Object.getOwnPropertySymbols()
返回基于符号的键值组成的数组,
Reflect.ownKeys()
返回所有键值,包含符号键值,组成的数组。
Object.getOwnPropertySymbols(user);
// <-- Array [ Symbol() ]
Reflect.ownKeys(user)
// <-- Array [ "name", "age", Symbol() ]
有名的符号
因为以符号做为键值的属性在ES6之前的代码中是不可见的,所以在保证向后兼容的同时,符号是给JavaScript现有类型添加新功能的理想选择。所谓“有名”的符号是预定义在
Symbol
函数上的属性,它们被用来自定义某些语言特性的行为,实现新的功能,比如迭代器。
Symbol.iterator
是一个有名的符号,被用来给对象添加一个特殊的方法,使得对象可以被迭代:
const band = ['Freddy', 'Brian', 'John', 'Roger'];
const iterator = band[Symbol.iterator]();
iterator.next().value;
// <-- { value: "Freddy", done: false }
iterator.next().value;
// <-- { value: "Brian", done: false }
iterator.next().value;
// <-- { value: "John", done: false }
iterator.next().value;
// <-- { value: "Roger", done: false }
iterator.next().value;
// <-- { value: undefined, done: true }
内建类型字符串,数组,类型数组,Map(映射)和Set(集合)都有一个默认的
Symbol.iterator
方法。当上述类型的实例被
for ... of
循环或者用于扩展运算符的时候就会调用这个方法。浏览器也开始使用
Symbol.iterator
键值让DOM结构,比如
NodeList
和
HTMLCollection
以同样的方式被迭代。
全局注册表
规范还定义了一个运行时范围的符号注册表,这意味着你可以在不同的执行上下文,比如在文档、内嵌iframe或者service worker之间,存储和获取符号。
Symbol.for(key)
用来获取注册表中给定键值的符号。如果对应这个键值的符号不存在,会返回一个新的符号。正如你预期的那样,对于同一个值,后续的调用都会返回同一个符号。
Symbol.keyFor(symbol)
允许你获取给定符号的键值。如果注册表中不存在给定的符号,那么会返回
undefined
:
const debbie = Symbol.for('user');
const mike = Symbol.for('user');
debbie === mike
// <-- true
Symbol.keyFor(debbie);
// <-- "user"
用例
在一些用例中,使用符号提供了优势。其中之一是在文章开头提到的,当给一个对象添加不想被对象序列化的时候包含在内的属性,就好像属性是“隐藏”的时候,使用符号就是一个很好的选择。