ES6 深入:符号

ES6 深入 是一个系列,探讨 ECMAScript 标准第 6 版(简称 ES6)中新增的 JavaScript 编程语言的新特性。

注意:现在有一个 越南语 版本的这篇文章,由 Coupofy 团队 的 Julia Duong 创作。

ES6 符号是什么?

符号不是徽标。

它们不是您可以在代码中使用的图片。

let 😻 = 😍 x 😺; // SyntaxError

它们不是代表其他事物的文学手段。

它们绝对不是与铙钹相同的东西。

(在编程中使用铙钹不是一个好主意。它们很容易发生崩溃。)

那么,符号究竟是什么?

第七种类型

自从 JavaScript 在 1997 年首次标准化以来,已经出现了六种 类型。在 ES6 之前,JS 程序中的每个值都属于这些类别之一。

  • 未定义
  • 布尔值
  • 数字
  • 字符串
  • 对象

每种类型都是一组值。前五个集合都是有限的。当然,只有两个布尔值,truefalse,而且它们不会创建新的布尔值。数字和字符串值的数量要多得多。标准规定有 18,437,736,874,454,810,627 个不同的数字(包括 NaN,其名称是“非数字”的缩写)。与可能存在的不同字符串的数量相比,这不算什么,我认为是 (2144,115,188,075,855,872 − 1) ÷ 65,535 …虽然我可能数错了。

但是,对象的集合是开放式的。每个对象都是一个独特的、珍贵的雪花。每次您打开网页时,都会创建大量新对象。

ES6 符号是值,但它们不是字符串。它们也不是对象。它们是全新的东西:第七种类型的值。

让我们讨论一个它们可能派上用场的场景。

一个简单的布尔值

有时,在 JavaScript 对象上存储一些额外的数据会非常方便,而这些数据实际上属于其他人。

例如,假设您正在编写一个 JS 库,该库使用 CSS 过渡让 DOM 元素在屏幕上快速移动。您注意到,尝试将多个 CSS 过渡同时应用于单个 div 无法正常工作。它会导致难看的、不连续的“跳跃”。您认为可以修复这个问题,但首先您需要一种方法来找出给定元素是否已经在移动。

如何解决这个问题?

一种方法是使用 CSS API 向浏览器询问元素是否正在移动。但这听起来太复杂了。您的库应该已经知道元素正在移动;毕竟是它让元素开始移动的!

您真正想要的是一种方法来跟踪哪些元素正在移动。您可以保留一个所有移动元素的数组。每次调用您的库来动画化元素时,您可以搜索该数组以查看该元素是否已经存在。

嗯。如果数组很大,线性搜索会很慢。

您真正想做的是在元素上设置一个标志

if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

 

这样也有一些潜在的问题。它们都与您的代码并非唯一使用 DOM 的代码这一事实有关。

  1. 使用 for-inObject.keys() 的其他代码可能会遇到您创建的属性。
  2. 其他一些聪明的库作者可能已经想到过这种技术,您的库可能会与该现有库产生不良互动。
  3. 其他一些聪明的库作者将来可能会想到这种技术,您的库可能会与该未来的库产生不良互动。
  4. 标准委员会可能会决定向所有元素添加 .isMoving() 方法。到那时您就真的完蛋了!

当然,您可以通过选择一个如此繁琐或如此愚蠢的字符串来解决最后三个问题,以至于其他人永远不会将任何东西命名为

if (element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__) {
  smoothAnimations(element);
}
element.__$jorendorff_animation_library$PLEASE_DO_NOT_USE_THIS_PROPERTY$isMoving__ = true;

 

这似乎不值得眼睛疲劳。

您可以使用加密技术为属性生成一个实际上唯一的名称

// get 1024 Unicode characters of gibberish
var isMoving = SecureRandom.generateName();

...

if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

 

object[name] 语法允许您使用任何字符串作为属性名称。因此,这将起作用:碰撞几乎不可能发生,而且您的代码看起来还不错。

但这样做会导致糟糕的调试体验。每次您对具有该属性的元素使用 console.log() 时,您都会看到一大堆垃圾字符串。如果需要不止一个这样的属性怎么办?如何区分它们?每次您重新加载时,它们都会有不同的名称。

为什么这么难?我们只需要一个简单的布尔值!

符号是答案

符号是程序可以创建并用作属性键的值,而不必担心名称冲突。

var mySymbol = Symbol();

 

调用 Symbol() 会创建一个新的符号,该符号的值与其他任何值都不相等。

就像字符串或数字一样,您可以将符号用作属性键。因为它与任何字符串都不相等,所以保证该符号键属性不会与任何其他属性发生冲突。

obj[mySymbol] = "ok!";  // guaranteed not to collide
console.log(obj[mySymbol]);  // ok!

 

以下是如何在上面讨论的情况下使用符号

// create a unique symbol
var isMoving = Symbol("isMoving");

...

if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

 

关于这段代码的一些说明

  • Symbol("isMoving") 中的字符串 "isMoving" 称为 描述。它有助于调试。当您将符号写入 console.log() 时,当您使用 .toString() 将其转换为字符串时,以及可能在错误消息中,它都会显示出来。仅此而已。
  • element[isMoving] 称为 符号键属性。它只是一个名称为符号而不是字符串的属性。除此之外,它在各个方面都是一个正常的属性。
  • 与数组元素一样,符号键属性不能使用点语法访问,例如 obj.name。它们必须使用方括号访问。
  • 如果您已经拥有符号,那么访问符号键属性非常简单。上面的示例显示了如何获取和设置 element[isMoving],如果需要,我们还可以询问 if (isMoving in element) 甚至 delete element[isMoving]
  • 另一方面,所有这些操作都只有在 isMoving 在作用域内时才有可能。这使得符号成为一种弱封装机制:一个为自己创建了一些符号的模块可以在它想要的任何对象上使用这些符号,**而无需担心与其他代码创建的属性发生冲突**。

由于符号键旨在避免冲突,因此 JavaScript 最常见的对象检查功能会简单地忽略符号键。例如,for-in 循环仅遍历对象的字符串键。符号键将被跳过。Object.keys(obj)Object.getOwnPropertyNames(obj) 也执行相同的操作。但符号并不完全是私有的:可以使用新的 API Object.getOwnPropertySymbols(obj) 来列出对象的符号键。另一个新的 API,Reflect.ownKeys(obj),返回字符串键和符号键。(我们将在即将发布的文章中全面讨论 Reflect API。)

库和框架可能会找到符号的许多用途,正如我们将在后面看到的,语言本身也将其用于各种目的。

但是,符号究竟是什么?

> typeof Symbol()
"symbol"

 

符号与其他任何东西都不完全相同。

一旦创建,它们就是不可变的。您不能在它们上设置属性(如果在严格模式下尝试这样做,您将得到一个 TypeError)。它们可以是属性名称。这些都是类似字符串的特性。

另一方面,每个符号都是唯一的,与所有其他符号(即使是具有相同描述的其他符号)不同,并且您可以轻松地创建新的符号。这些是类似对象的特性。

ES6 符号类似于 Lisp 和 Ruby 等语言中的 更传统的符号,但没有那么紧密地集成到语言中。在 Lisp 中,所有标识符都是符号。在 JS 中,标识符和大多数属性键仍然被认为是字符串。符号只是一个额外的选项。

关于符号的一个快速警告:与语言中的几乎所有其他东西不同,它们不能自动转换为字符串。尝试将符号与字符串连接会导致 TypeError。

> var sym = Symbol("<3");
> "your symbol is " + sym
// TypeError: can't convert symbol to string
> `your symbol is ${sym}`
// TypeError: can't convert symbol to string

 

您可以通过显式地将符号转换为字符串来避免这种情况,即编写 String(sym)sym.toString()

三组符号

有三种方法可以获得符号。

  • 调用 Symbol()正如我们已经讨论过的,每次调用它都会返回一个新的唯一符号。
  • 调用 Symbol.for(string)这将访问一组称为 符号注册表 的现有符号。与 Symbol() 定义的唯一符号不同,符号注册表中的符号是共享的。如果您调用 Symbol.for("cat") 三十次,它将每次返回相同的符号。当多个网页或同一网页中的多个模块需要共享符号时,该注册表很有用。
  • 使用标准定义的符号,例如 Symbol.iterator标准本身定义了一些符号。每个符号都有其自身的特殊用途。

如果您仍然不确定符号是否真的有用,那么最后一类很有趣,因为它们展示了符号在实践中如何已经证明其有用性。

ES6 规范如何使用众所周知的符号

我们已经看到 ES6 使用符号来避免与现有代码发生冲突的一种方法。几周前,在 关于迭代器的文章 中,我们看到循环 for (var item of myArray) 通过调用 myArray[Symbol.iterator]() 开始。我提到过,这个方法可以被称作 myArray.iterator(),但使用符号可以更好地实现向后兼容性。

现在我们已经了解了符号的本质,很容易理解为什么这样做以及这意味着什么。

以下是 ES6 使用众所周知的符号的其他几个地方。(这些功能尚未在 Firefox 中实现。)

  • 使 instanceof 可扩展。在 ES6 中,表达式 <var>object</var> instanceof <var>constructor</var> 被指定为构造函数的一种方法:constructor[Symbol.hasInstance](object)。这意味着它是可扩展的。
  • 消除新功能与旧代码之间的冲突。这非常晦涩难懂,但我们发现某些 ES6 Array 方法仅仅因为存在而破坏了现有的网站。其他 Web 标准也有类似的问题:仅仅在浏览器中添加新方法就会破坏现有网站。但是,这种破坏主要由称为 动态作用域 的东西引起,因此 ES6 引入了一个特殊的符号,Symbol.unscopables,Web 标准可以使用它来阻止某些方法参与动态作用域。
  • 支持新的字符串匹配方式。 在 ES5 中,str.match(myObject) 试图将 myObject 转换为 RegExp。 在 ES6 中,它首先检查 myObject 是否具有 myObject[Symbol.match](str) 方法。 现在,库可以提供自定义的字符串解析类,这些类可以在所有使用 RegExp 对象的地方工作。

每个使用都非常狭窄。 很难看到这些特性本身对我的日常代码有重大影响。 长远来看,更有趣的是,众所周知的符号是 JavaScript 对 PHP 和 Python 中 __双下划线 的改进版本。 标准将在未来使用它们在语言中添加新的钩子,而不会对现有代码造成任何风险。

我什么时候可以使用 ES6 符号?

符号在 Firefox 36 和 Chrome 38 中实现。 我自己为 Firefox 实现它们,因此如果您的符号的行为像铙钹,您就知道该找谁了。

为了支持尚未原生支持 ES6 符号的浏览器,可以使用 polyfill,例如 core.js。 由于符号与语言中以前存在的任何东西都不完全相同,因此 polyfill 并不完美。 阅读注意事项。

下周,我们将发布 *两篇* 新文章。 首先,我们将介绍一些期待已久的特性,这些特性终于在 ES6 中加入 JavaScript — 并抱怨它们。 我们将从两个几乎可以追溯到编程黎明时期的特性开始。 我们将继续介绍两个非常相似的特性,但 *由 ephemerons 提供支持*。 因此,请在下周加入我们,深入了解 ES6 集合。

并且, 请继续关注 Gastón Silva 发布的附加文章,主题与 ES6 功能无关,但可能为您提供开始在自己的项目中使用 ES6 所需的推动。 那时见!

关于 Jason Orendorff

Jason Orendorff 的更多文章…


27 条评论

  1. Christoph

    精彩的文章系列!
    我不得不承认,当我读到关于编程中铙钹的双关语时,我咯咯地笑了 :)

    2015 年 6 月 12 日 下午 2:39

  2. David Madore

    这个特性不会造成无法回收的垃圾(我的意思是垃圾回收器永远无法回收的垃圾)吗? 能够 mykey = Symbol(),然后 anyobject[mykey] = myvalue,而无需担心密钥冲突的想法很棒,但如果将符号的私有性概念认真对待,那么无论何时 mykey 超出范围并变成垃圾,*任何以它为索引的东西都应该变成垃圾*,因为它们现在是未持有引用(如果希望每个子例程都能通过使用符号密钥自由地在任何类型的全局对象上放置临时数据,那么对它们的垃圾回收是必不可少的)。

    但是,Object.getOwnPropertySymbols() 的存在违背了符号私有的理念,我猜想也违背了它们索引的值变成垃圾(除非这种访问方式被视为一种弱键),所以看起来 ES6 的设计者对于他们试图实现的目标有不同的看法。(此外,“符号”这个名字非常令人困惑,“键”可能更好。或者,甚至更好,“symbolkey”……)

    2015 年 6 月 12 日 下午 3:14

    1. Samuel Reed

      我的猜测是,符号主要用于最后几段中描述的目的;现在我们有了一种非黑客的方式来添加新的原型特性并扩展语言,而无需担心使用字符串键可能会破坏旧代码,因为这些字符串键可能已经被现有的库使用。 仅此一项就值得了。

      我认为这里没有垃圾回收问题。 如果你将某样东西附加到一个对象,它就会附加到对象,直到该对象超出范围,就像使用任何字符串键一样,因为字符串是一个值,就像 Symbol 是一个值一样。 它不能“超出范围”,你也不会丢失对它的所有引用,因为它仍然可以通过枚举找到。

      是否可以向对象添加符号属性,但使其不可枚举? 可能,可以使用 Object.defineProperty — 你可以附加数据并使其真正隐藏。

      2015 年 6 月 12 日 下午 6:35

    2. Jason Orendorff

      David:你说得对,垃圾回收是一个需要关注的问题。 由于符号键属性在各个方面都与普通属性相同,它们的生命周期与它们所附加的对象一样长。 如果您的代码将许多属性附加到生命周期较长的对象 — 无论是否使用符号 — 内存使用情况是需要密切关注的事情。

      在这方面,属性就像事件监听器。

      ES6 中没有弱引用,但有 WeakMap,它与这个用例非常相关。 我将在下周撰写这方面的文章。

      无论如何,只要创建的符号数量很少,这不太可能成为问题。 请参阅 http://codepen.io/anon/pen/oXwQOq 获取示例。 这使用的内存量并不夸张,如果一个元素被垃圾回收,它 的符号键属性也会被垃圾回收。

      2015 年 6 月 12 日 上午 9:30

  3. tgc

    非常有趣的文章!

    一个问题:在 DOM 元素上设置属性是不是不受欢迎? 我看到符号解决了字符串键的命名冲突问题,但 IIRC 宿主对象不应该被修改,因为它们的行为没有在规范中定义。

    2015 年 6 月 12 日 下午 3:35

  4. Flimm

    直到我遇到符号注册表,我才明白符号的原理。 如果符号的全部目的是避免选择与其他代码现在或将来可能选择的相同字符串名称,那么这个注册表肯定会破坏它? 注册表的键是字符串,有可能发生冲突。

    2015 年 6 月 12 日 下午 5:16

    1. Samuel Reed

      我认为现在的唯一区别是,符号注册表是完全开放的,而许多字符串键已经被许多类型的对象保留了。

      看起来注册表是“运行时范围的”:也就是说,在任何页面上,调用 Symbol.for(“cat”) 始终会返回相同的符号。

      我同意你的反应。 看起来规范编写者在某一刻犹豫了一下,他们拥有了一个真正独一无二的对象,然后在最后一刻决定使它们更像字符串。 也许 Jason 可以评论一下?

      2015 年 6 月 12 日 下午 6:38

    2. William

      我也有同样的问题,找到了答案(至少根据当前的 Firefox 行为)。

      试试这个
      let a = Symbol(‘cat’)
      let b = Symbol(‘cat’)
      let c = Symbol.for(‘cat’)
      let d = Symbol.for(‘cat’)
      console.log(a === b) // false
      console.log(a === c) // false
      console.log(c === d) // true

      现在,如果你的模块想要共享一个符号,而其他模块也想共享这个符号,那么我认为你会有问题。 但如果你只是使用 Symbol(‘name’),那么看起来你会没事。

      2015 年 6 月 12 日 上午 9:07

    3. CD

      这是一个有效的观点,但我在文章的第一遍阅读中没有注意到的是,只有在你明确使用“`Symbol.for(string)“` 语法时,符号才会被添加到注册表中。

      因此,这种 Symbol 创建方法应该谨慎使用,只在你想在外部共享 Symbol 并意识到潜在冲突的情况下使用。

      我做了一个 CodePen 来演示这一点:http://codepen.io/chrisdeely/pen/VLWqEO?editors=001

      2015 年 6 月 12 日 上午 10:51

    4. Jason Orendorff

      我认为这里的设计意图是,有时程序会希望符号的其他特性 — 特别是符号键属性不会被 for-in 访问,并且很难意外访问 — 但需要能够跨 iframe 或跨单个窗口内的库访问该属性。

      当你需要这样的时候,而且你并不太关心唯一性,你可以使用 Symbol.for()。

      2015 年 6 月 12 日 下午 3:12

  5. realityking

    忽略核心语言钩子,在我的代码中,使用 Symbol 向对象添加值与使用 Map 或 WeakMap 相比,有什么优势? 这将解决 tgc 关于垃圾回收的担忧,并防止向对象添加新属性,这似乎会让 JIT 编译器崩溃。

    2015 年 6 月 12 日 下午 5:17

  6. javascript coder

    如果 JavaScript 语言中只有一种方法可以从 Object 返回每个实例的唯一哈希值 — 一个递增的 64 位值就足够了 — 那么引入 Symbol 就没有必要。 尝试在 javascript 中建立对象标识非常繁琐且效率低下 — 你基本上必须搜索所有元素并使用 === 运算符与它们进行比较。 请参阅 Crockfords 的 decycle 函数 https://github.com/douglascrockford/JSON-js/blob/master/cycle.js#L69 获取示例。 将 Symbol 添加到 ECMAScript 并没有帮助这种情况。

    2015 年 6 月 12 日 下午 7:45

  7. Kyle Simpson

    我认为这里提出的主要用例,即使用你并不拥有的对象来标记额外的元数据,是一种反模式。

    ES6 有一个几乎完全针对此目的的功能:WeakMap。 使用第三方对象作为你自己的 WM 中的键,并将它与你需要的任何类型的数据关联起来。 O(1) 查找。 更好的内存管理。

    修改你并不拥有的对象,即使使用不可猜测、不可冲突的键,也是一种不好的做法。 使用合适的工具完成合适的工作。

    尽管如此,我认为符号对于使用元数据注释你 *自己* 的对象非常有用,特别是在使它更能抵抗冲突或外部元编程方面。 我很高兴这篇文章指出了这个特性本身,我只是希望展示一个不同的用例。

    2015 年 6 月 12 日 上午 9:50

    1. Jason Orendorff

      我将在下周撰写关于 WeakMaps 的文章,并将讨论权衡取舍。

      现在,我很难知道明天这些东西的最佳实践是什么。 实践需要一段时间才能收敛,并且有一个“足够好”的门槛,我很难对此做出预测。

      我当然同意 WeakMap 在将不同的东西分开方面做得很好,如果这就是你想要的,那么这就是你要走的路。 另外,如果一个对象被冻结,修改它甚至无法工作。 所以就是这样。

      性能方面不太清楚。两种情况下,查找操作的预期时间复杂度都应该是 O(1)。我不确定哪种数据结构更快(尽管如果非要猜的话,我认为缓存有利于属性访问)。不幸的是,WeakMaps 对垃圾回收性能不利。但我们正在努力改进它。

      2015 年 6 月 12 日 下午 2:58

  8. Andrea Giammarchi

    唯一一个在旧版移动浏览器中也能正常工作的 Symbol polyfill
    https://github.com/WebReflection/get-own-property-symbols

    请阅读 Caveat 部分,因为它也涵盖了 core.js shim 的部分内容。

    例如,你不能对 null 对象使用 Symbols。即使使用 Babel 和 core.js 也不能,因为它们会失败。

    typeof 在 core.js 中不起作用,在我的 polyfill 中也不起作用……**等等等等**

    但是,这些功能运行得非常完美。

    2015 年 6 月 12 日 上午 10:20

  9. MrD

    @Flimm,我刚测试了一下,似乎注册表只有在使用 Symbols.for() 时才有效,因此,如果你不想出现冲突,只需使用 Symbol()

    var a = Symbol(“foo”);
    var b = Symbol.for(“foo”);
    var c = Symbol(“foo”);
    var d = Symbol.for(“foo”);
    a == b; // false
    a == c; // false
    a == d; // false
    b == d; // true
    b === d; // true

    2015 年 6 月 12 日 下午 12:14

  10. David Bonnet

    感谢你的介绍。为什么使用 “@@” 作为内建符号的简写,例如 `@@iterator` 代表 `Symbol.iterator`?你能详细说明一下吗?

    2015 年 6 月 15 日 上午 4:40

    1. Jason Orendorff

      当然!@@iterator 不是你在 JS 代码中可以写的东西。它是一种在 ES6 语言规范中使用的技术符号。它的含义是“作为 Symbol.iterator 的初始值的符号”。

      ES6 规范始终将 Symbol.iterator 称为“@@iterator”,以确保精确性。脚本可以 `delete Symbol` 或以其他方式篡改全局变量,但 @@iterator 永远不会改变。

      http://people.mozilla.org/~jorendorff/es6-draft.html#sec-well-known-symbols

      类似地,ES6 规范在 ES5 中使用 “Array.prototype” 的地方也使用了 “%ArrayPrototype%”。许多对象都采用了这种处理方式。

      http://people.mozilla.org/~jorendorff/es6-draft.html#sec-well-known-intrinsic-objects

      即使在与其他 JS 引擎黑客的讨论中,我通常也会避免这种符号,而直接写 “Symbol.iterator” 或 “Array.prototype”。但如果你要查看规范,就必须了解这种符号。

      2015 年 6 月 16 日 上午 5:28

      1. David Bonnet

        原来它是规范特有的……感谢你对此进行澄清!

        2015 年 6 月 16 日 上午 5:40

  11. Brett Zamir

    你的长属性名让我想起了这幅《远方》漫画:http://i674.photobucket.com/albums/vv101/Konradius5/Gary%20Larson%20Comics/InsaneBranding.jpg

    2015 年 6 月 15 日 下午 4:32

  12. Yanis

    感谢你写了这篇文章。我刚刚更新了我很久以前在 stackoverflow 上提出的问题,其中包含了这篇文章的链接。
    http://stackoverflow.com/questions/21724326/why-bring-symbols-to-javascript

    2015 年 6 月 16 日 上午 1:27

  13. bent

    null 的类型是 object

    2015 年 6 月 30 日 下午 12:46

  14. Michael

    似乎应该有一个 Symbol.for(obj,name),因为我可以理解在某个范围内共享符号而不使其成为全局符号的需求。

    2015 年 7 月 1 日 下午 12:11

  15. Max Battcher

    @Michael,如果你需要一个“作用域”内的 Symbol,目前最好的方法是依赖模块系统,在某个模块中导出你的符号,并在需要使用该符号时导入它。幸运的是,ES6 也包含了模块系统。

    // a.js
    export var mySymbol = Symbol(‘mySymbol’);

    // b.js
    import { mySymbol } from ‘a’;

    2015 年 7 月 6 日 上午 11:20

  16. roland

    为了确保我没有错过文章中的任何要点

    > var abc=Symbol(‘123’);
    abc
    abc
    两种不同的方式,尽管两个引擎都返回
    >abc.toString()+”1″
    <"Symbol(123)1"

    因此,这与你上面描述的关于转换为字符串的方式不完全一致。尤其是因为“123”更像是一个键值对…….

    最后一点:我还使用 node.js 0.12.6 进行了测试,结果与 Chrome43.0 相似!

    2015 年 7 月 9 日 上午 9:11

    1. Jason Orendorff

      每次调用 Symbol() 时都会创建一个新的符号。但如果你只调用一次,就只会创建一个符号。

      > var abc = Symbol(“123”);
      > abc === abc
      true

      2015 年 7 月 9 日 上午 10:25

  17. Michael

    如果你能从你的符号中读取回你的描述字符串,那就很有用——方便在格式化消息等操作中使用。现在你几乎必须进行 toString() 并删除 Symbol() 部分,这显得过于繁琐。

    2015 年 7 月 10 日 下午 1:06

本文的评论已关闭。