学习 ES6:第七种数据类型–符号(Symbols)

自1997年 JavaScript 发明以来,一直只有六种数据类型,在 ES6 中,出现了第七种数据类型:符号(Symbols)。

参考博文:ES6 In Depth:SymbolsMDN:Symbol

自从1997年 JavaScript 语言第一次被标准化,就六种数据类型,到 ES6 标准确立之前,每一个 JavaScript 变量都可以在其中找到归属。

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Object

每种类型都是一组值,前五种类型的数据值的数量都是有穷的。布尔型(Boolean)只有两个值,truefalse,并且目前也不会有新的值出现,然后是数字型(Number)与字符型(String),JavaScript 的标准文档中描述有18,437,736,874,454,810,627个不同的数字(包括NaN,”Not a Number”的数字型变量的简称)。但最多的还是字符型,估计有(2144,115,188,075,855,872 − 1) ÷ 65,535种可能,甚至更多,虽然不一定正确,但字符串类型值的集合一定是有限的。

但是对象类型的值是可变的,第一个对象都不相同,就像雪花一样;每次你打开一个新的网页,就会有新的对象被创建。

ES6 中的符号值既不是字体,也不是对象,他是新的东西:第七种数据类型。

让我们模拟一个字符型(Symbol)可以派上用场的情景。

从一个简单的布尔类型出发

有时候你可以非常轻松地将别人的外部数据存储到一个 JavaScript 对象中。

举 个例子,假设你正在写一个 JavaScript 库,可以通过 CSS transitions 使 DOM 元素在屏幕上移动。你可能会注意到,当你尝试在一个 div 元素上同时应用多重 CSS transitions 时并不会生效。实际效果是丑陋而又不连续的“跳闪”。你认为可以修复这个问题,但前提是你需要一种发现给定元素是否已经移动过的方法。

应当如何解决这个问题呢?

一种方法是,用 CSS API 来告诉浏览器元素是否正在移动,但这样简直小题大做。在元素移动的第一时间内你的库就应该记录下移动的状态,所以它自然知道元素正在移动。

你真正想要的是一种持续跟踪某个元素正在移动的方法。你可以维护一个数组,记录所有正在移动的元素,每当你的库被调用来移动某个元素时,你可以检索数组来查看元素是否已经存在,亦即它是否正在移动中。

当然,如果数组非常大的话,线性搜索将会非常缓慢。

实际上你只想为元素设置一个标记:

这样也会有一些潜在的问题,事实上,你的代码很可能不是唯一一段操作 DOM 的代码。

  1. 你创建的属性很可能影响到其它使用了for-inObject.keys()的代码
  2. 一些聪明的库作者可能已经考虑并使用了这项技术,这样一来你的库就会与已有的库产生某些冲突
  3. 当然,很可能你比他们更聪明,你先采用了这项技术,但是他们的库仍然无法与你的库默契配合
  4. 标准委员会可能决定为所有的元素增加一个.isMoving()方法,到那时你需要重写相关逻辑,必定会有深深的挫败感

当然你可以选择一个乏味而愚蠢的命名(其他人根本不会想用的那些名称)来解决最后的三个问题:

这只会造成无畏的眼疲劳。借助于密码学,你可以生成一个唯一的属性名称:

object[name]语法允许你使用几乎任何字符串作为属性名称。所以这个方法行之有效:冲突几乎是不可能的,并且你的代码看起来也很简洁。

但是这也将带来不良的调试体验。每当你在控制台输出(console.log())包含那个属性的元素时,你将会看到一堆巨大的字符串垃圾。假使你需要比这多得多的类似属性呢?你如何保持它们整齐划一?每当你重载的时候它们的命名甚至都不一样!

为什么这个问题如此困难?我们只想要一个小小的布尔值啊!

Symbol 是终极解决方案

symbol是程序创建并且可以用作属性键的值,并且它能避免命名冲突的风险。

调用Symbol()创建一个新的 symbol,它的值与其它任何值皆不相等。

字符串或数字可以作为属性的键,symbol 也可以,它不等同于任何字符串,因而这个以 symbol 为键的属性可以保证不与任何其它属性产生冲突。

想要在上述讨论的场景中使用 symbol,你可以这样做:

有关这段代码的一些解释:

  • Symbol("isMoving")中的isMoving被称作描述。你可以通过console.log()将它打印出来,对调试非常有帮助;你也可以用.toString()方法将它转换为字符串呈现;它也可以被用在错误信息中。
  • element[isMoving]被称作一个以symbol为键(symbol-keyed)的属性。简而言之,它的名字是symbol而不是一个字符串。除此之外,它与一个普通的属性没有什么区别。
  • 如果你已经得到了symbol,那么访问一个以symbol为键的属性同样简单,以上的示例很好地展示了如何获取element[isMoving]的值以及如何为它赋值。如果我们需要,可以查看属性是否存在:if (isMoving in element),也可以删除属性:delete element[isMoving]
  • 另一方面,只有当isMoving在当前作用域中时才会生效。这是symbol的弱封装机制:模块创建了几个symbol,可以在任意对象上使用,无须担心与其它代码创建的属性产生冲突。

symbol 键的设计初衷是避免冲突,因此 JavaScript 中最常见的对象检查的特性会忽略 symbol 键。例如,for-in循环只会遍历对象的字符串键,symbol 键直接跳过,Object.keys(obj)Object.getOwnPropertyNames(obj)也是一样。但是symbols 也不完全是私有的:用新的 API Object.getOwnPropertySymbols(obj)就可以列出对象的 symbol 键。另一个新的 API,Reflect.ownKeys(obj),会同时返回字符串键和 symbol 键。

到底什么是symbol?

确切地说,symbol 与其它类型并不完全相像。

symbol 被创建后就不可变更,你不能为它设置属性(在严格模式下尝试设置属性会得到 TypeError 的错误)。他们可以用作属性名称,这些性质与字符串类似。

另一方面,每一个 symbol 都独一无二,不与其它 symbol 等同,即使二者有相同的描述也不相等;你可以轻松地创建一个新的 symbol。这些性质与对象类似。

ES6 中的 symbol 与 Lisp 和 Ruby 这些语言中更传统的 symbol 类似,但不像它们集成得那么紧密。在 Lisp 中,所有的标识符都是 symbol;在 JavaScript 中,标识符和大多数的属性键仍然是字符串,symbol 只是一个额外的选项。

关于 symbol 的忠告:symbol 不能被自动转换为字符串,这和语言中的其它类型不同。尝试拼接 symbol 与字符串将得到 TypeError 错误。

通过String(sym)sym.toString()可以显示地将symbol转换为一个字符串,从而回避这个问题。

获取 symbol 的三种方法
  • 调用Symbol()。正如我们上文中所讨论的,这种方式每次调用都会返回一个新的唯一symbol。
  • 调用Symbol.for(string)。这种方式会访问 symbol 注册表,其中存储了已经存在的一系列 symbol。这种方式与通过Symbol()定义的独立 symbol 不同,symbol 注册表中的 symbol 是共享的。如果你连续三十次调用Symbol.for("cat"),每次都会返回相同的 symbol。注册表非常有用,在多个 web 页面或同一个 web 页面的多个模块中经常需要共享一个 symbol。
  • 使用标准定义的 symbol,例如:Symbol.iterator标准根据一些特殊用途定义了少许的几个 symbol。
symbol 在 ES6 规范中的应用

ES6 中还有其它几处使用了symbol 的地方。(这些特性在Firefox里尚未实现。)

  • 使instanceof可扩展。在 ES6 中,表达式object instanceof constructor被指定为构造函数的一个方法:constructor[Symbol.hasInstance](object)。这意味着它是可扩展的。
  • 消除新特性和旧代码之间的冲突。这一点非常复杂,但是我们发现,添加某些 ES6数组方法会破坏现有的Web网站。其它 Web 标准有相同的问题:向浏览器中添加新方法会破坏原有的网站。然而,破坏问题主要由动态作用域引起,所以 ES6 引入一个特殊的symbol——Symbol.unscopables,Web 标准可以用这个 symbol 来阻止某些方法别加入到动态作用域中。
  • 支持新的字符串匹配类型。在 ES5 中,str.match(myObject)会尝试将myObject转换为正则表达式对象(RegExp)。在 ES6 中,它会首先检查myObject是否有一个myObject[Symbol.match](str)方法。现在的库可以提供自定义的字符串解析类,所有支持RegExp
文后小结

symbol 在 Firefox 36 和 Chrome 38 中均已被实现,为了支持那些尚未支持原生 ES6 symbol 的浏览器,你可以使用一个 polyfill,例如 core.js。因为 symbol 与其它类型不尽相同,所以 polyfill 目前不是很完美,请阅读注意事项

发表评论