虽然 ES2015 已经引入了许多开发人员期待已久的语言特性, 但还有一些新特性不太为人所知和理解, 其好处也不太清楚 -- 比如 symbols.
symbol(符号)是一种新的原始数据类型, 一个确保不会和其它符号冲突的唯一令牌. 从这个意义上讲, 你可以把符号看作是一种 UUID https://en.wikipedia.org/wiki/Universally_unique_identifier (通用唯一识别码). 让我们看看符号是如何工作的, 以及我们能用它做些什么.
创建符号
创建符号非常直接, 就是简单地调用 https://developer.mozilla.org/en/docs/web/JavaScript/Reference/Global_Objects/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 https://en.wikipedia.org/wiki/Hash_table (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] = 'fred@example.com';
- 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"
用例
在一些用例中, 使用符号提供了优势. 其中之一是在文章开头提到的, 当给一个对象添加不想被对象序列化的时候包含在内的属性, 就好像属性是 "隐藏" 的时候, 使用符号就是一个很好的选择.
代码库的作者可以放心地使用符号给客户端对象添加属性和方法, 而不用担心覆盖已有的键值 (或者自己添加的键值被别的代码覆盖). 例如, 小部件组件(比如时间选择器) 在初始化的时候会产生很多选项和状态, 这些选项和状态需要存储在某个地方. 把组件实例赋值给 DOM 元素对象的一个属性并不是理想的选择, 因为这个属性有可能会和其它的键值冲突. 使用基于符号的键值可以巧妙地解决这个问题, 确保组件实例不会被覆盖. 有关这个想法更详细的探索, 请查看 Mozilla Hacks 的博客 ES6 in Depth: Symbols https://hacks.mozilla.org/2015/06/es6-in-depth-symbols/ .
浏览器支持情况
如果你想体验符号, 主流浏览器的支持是很好的 https://kangax.github.io/compat-table/es6/ . 如你所见, Chrome,Firefox,Microsoft Edge 和 Opera 的当前版本, 移动设备上的 Android 5.1 和 iOS 9 都原生支持符号了. 如果需要兼容较老版本的浏览器, 可以使用腻子脚本 https://github.com/medikoo/es6-symbol .
总结
虽然引入符号的主要原因似乎是为了在兼容现有代码的前提下为语言添加新功能, 但它们确实有一些有趣的用途. 对于所有开发人员来说, 至少对它们有一个基本的了解, 并且熟悉最常用的, 有名的符号及其用途是值得的.
来源: https://juejin.im/entry/5b8671aa51882542ee71750d