ES6 in Depth: Symbols

ES6 in Depth: Symbols

ES6 in Depth 是介绍 ES6(ECMAScript 标准的第6版本缩写) 的 JS 新特性的一系列文章。

备注:这文章是由 Coupofy team 的 JuliaDuong 编写,有越南语版本

什么是 ES6 的 symbols?

Symbols 并不是 Logos。

在你的代码中,你或许想到你可以使用些不小的图片:

1
let 😻 = 😺 × 😍; // SyntaxError

此处省略…

那么,什么是 Symbol呢?

第七种类型

自1997年的JS第一次标准化以来,它就包含有六种类型。直到 ES6,JS编程中的每个值都会在这些类别中。

  • Undefined 未定义
  • Null 空
  • Boolean 布尔
  • Number 数值
  • String 字符串
  • Object 对象

每个类型是其值的一个集合,前5个集合是有限的。其中,只有两个布尔值, true 和 false ,他们并没有增加新的布尔值。值 更多的有 数值 和 字符串。准确来说,有 18,437,736,874,454,810,627 不同的数值(包括 NaN,这数值的名称来自”Not a Number”的缩写)。相比字符串可能存在的数量, 我认为有 (2144,115,188,075,855,872 − 1) ÷ 65,535 – 尽管我有可能算错。

但是,对象值的集合是打开的。每个对象是唯一的,如珍贵的雪花。当你每次打开网页时,一堆的对象就会被创建。

ES6 symbols 也是值,但他们不是字符串,他们不是对象,他们是新的:第七种类型值。

让我们对随后来到的 ES6 symbol 的布置些场景。

一个简单、小的布尔

有时,它可以在JS对象中十分方便地存储一些临时的但实际属于其它的数据。

例如,你可以写一个JS库,用CSS的翻转(transitions)来让DOM元素在屏幕中收缩。你也注意到,当试图对一个div元素添加多个翻转时那是没有效果的。这显得很难受,时不时的”跳动“。你认为你可以修复这问题,但首先你需要找到一方法来判断这给定的元素是否已经移动了。

你怎么才能解决这问题?

一个方法是使用CSS的APIs 通过浏览器去查询这元素是否正在移动,但是这听起来很大刀。你的库应该已经知道这元素是正在移动的,那就是在你设置其移动的代码的第一个位置!

你可能希望的方法是实时跟踪移动的元素,你可以将所有移动的元素保存到一个数组中。当你库每次被调用来对一元素进行动画时,你可以查询一遍这个元素是否已经在数组中。

Hmm。如果这数组很大的话,这线性的检索是十分缓慢的。

你实际只是想做的是在元素的上面设置一个标识:

1
2
3
4
if (element.isMoving) {
  smoothAnimations(element);
}
element.isMoving = true;

这也有些潜在的问题,它们实际上有: 不仅仅是你的代码控制这些DOM。

  • 其它代码使用 for-in 或者 Object.keys()会使得你创建的属性出现困扰;
  • 某些其它明智库的作者可能已经考虑到了这一技术,然后这些已经存在的库会和你的库相互地作用,这是糟糕的。
  • 某些其它明智库的作者将来可能会想到这技术,然后你的库将来会出现冲突;
  • 标准委员会可能决定对所有的元素增加一个 .isMoving()方法,然后你真的湿身了;

好吧,你可以通过选择一个冗长而又笨拙的别人不用此来命名的字符串来解决后三个问题:

1
2
3
4
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;

但这样子很费眼。

你实际上可以通过密码学的方法生成一个唯一的字符串来命名这属性:

1
2
3
4
5
6
7
// get 1024 Unicode characters of gibberish
var isMoving = SecureRandom.generateName();
...
if (element[isMoving]) {
  smoothAnimations(element);
}
element[isMoving] = true;

object[name]语法可以让你使用任意字符串来标识这个属性的名称,所以这代码是可用的:冲突几乎不可能出现,你的代码看起来很好。

但这将导致一个调试不好的体验,当你每次用console.log()来输出一个元素的这属性时,你将会看到大量的字符串堆在垃圾桶里。然后,如果你需要不仅仅是一个类似的属性时,那怎么办?你怎么保证它们是正常的?他们会在你每次重载时拥有不同的名称。

为什么会如此的艰难?我们只需要一个小小的布尔值而已!

Symbol(标志符) 是答案

程序可以创建标志符,来作为属性的关键字,而不会引起命名冲突:

1
var mySymbol = Symbol();

调用Symbol()可创建一个新的标志符,其值不会与其它任何值相等。

就像一个字符串或者数值,你可以使用标志符来作为属性的key,因为它不会与其它的值相等,所以使用标志符作为属性的key不会与其它属性有冲突。

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

这里,你可以用标志符来解决之前的问题:

1
2
3
4
5
6
7
8
// create a unique symbol
var isMoving = Symbol("isMoving");
...

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

这代码有些要注意的点:

  • Symbol("isMoving")中的字符串”isMoving”表示为一个描述信息,这可用于调试。

  • element[isMoving]称为一个 symbol-keyed 属性,它是一个用标志符而不是字符串来命名的属性。除此之外,它是一个正常的属性(没什么区别)。

  • 类似数组,symbol-keyed 属性不能通过 点的语法obj.name来访问,必须使用中括号来访问。

  • 如果你熟悉了标志符,你可以直接地使用 symbol-keyed 属性。之前的例子中已经展示怎么获取和设置element[isMoving],如果我们需要,我们还可以使用 if (isMoving in element)或者甚至是delete element[isMoving]

  • 另外,只要isMoving在作用域中以上的操作都是可行的,这使得标志符拥有个弱封装的机制:一个模块内创建一些标志符,则可用于其所有的对象中,不用担心其他代码的属性创建而引起冲突。

因为标志符在设计时就是要避免冲突的,JS多数对象注入的特性忽略标志符的key的。例如,一个for-in的循环,只有循环会对对象的非字符串key起作用。标志符的key是被跳过的,Object.key(obj)ObjectgetOwnPropertyNames(obj)也是一样的。但是,标志符实际上并不是私有的:它可以使用新的API Object.getOwnPropertySymbos(obj)来列出一个对象的所以标志符的key。另外新的API,Reflect.ownKeys(obj),返回包括字符串和标志符的keys。(我们将会在即将到来的文章中全方位地讨论Reflect API。)

库和框架将会挖掘出许多的标志符的用法,然后,我们将会看到JS把标志符应用于更大的范围和目标中。

但是,标志符实际上是什么

1
2
> typeof Symbol()
"symbol"

标志符并不同其它东西。

它们在创建后就是不可变的。你不能为它们设置属性(如果你在严格模式中,你将会得到一个TypeError)。它们可以当作属性的名称,效果类似于字符串的key。

另外,每个标志符是唯一的,区别于其它标志符(即使其它的标志符拥有相同的描述信息),你可以轻松地创建新的标志符,这类似于对象。

ES6的标志符类似于其它语言的更为传统标志符 ,如Lisp和Ruby,但是并非完全接近这些语言。在Lisp中,所有的身份标识都是标志符。在JS,身份标识和多数属性的key经过考虑之后,依然定为是字符串。标志符只是额外的选项。

标志符的有个注意的地方:并不像其它语言一般,JS中的标志符不能自动转为字符串。试图将一个标志符和字符串拼接时,将得到得到 TypeError。

1
2
3
4
5
> 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 registry)的标志符。不像Symbol()会得到唯一的标志符,在标志符登记处中的标志符是可分享的。如果你调用Symbol.for("cat")三十次时,你在每次得到的是同一个标志符。当多个页面、或者多个模块位于同一页面时,这登记处就十分有用的,因为它们需要分享标志符。

  • Symbol.iterator来使用标志符定义标准。标准库中只定义了部分标志符,但每个都是有其目的的。

如果你依然没有确定标志符是怎么有作用的,最后一项就有意义了,因为它展示了 标志符在应用中得到证明是有用的。

ES6是怎么使用著名的标志符

我们已经看到在 ES6 中使用标志符可以避免与现有的代码产生冲突。几周前,在 迭代器的文章 中,我们看到循环for (var item of myArray)在开始时调用myArray[Symbol.iterator]()。我提到,这方法会在myArray.iterator()时调用到,因为标志符可以更好地向后兼容。

现在,我们知道了标志符的所有信息,很容易理解它是为什么和是什么。

这里还指出其它地方是怎么使用ES6著名的标志符的。(这些特性未在Firefox实现。)

  • 扩展 instanceof。在ES6中,表达式objectinstanceofconstructor在构造器(constructor)里面有一指定方法:constructor[Symbol.hasInstance](object)。这就是扩展。

  • 消除新特性与旧代码之间的冲突。这意思非常模糊,但是我们已经确定的是,ES6的Array方法在现有的网站就是不能运行的。其它网络标准也有类似的问题:简单地添加一个方法都会导致现有的网站崩溃。但是,这些问题主要是由于动态作用域导致的,所以,ES6推介使用特殊的标志符。

  • 支持新类型的string-matching。在ES5中,str.match(myObject)会试图将 myObject 转化为一个 RegExp。在ES6中,它会首先检查 myObject 是否有一个方法 myObjectSymbol.match。现在的库可以提供字符串转化的类,来支持RegExp对象能工作的地方也能正常运行。

每个用法都有些限制,很难看到这些特性在我每天的代码中起作用,但是从长远来看是可行的。著名的标志符对PHP和Python中的__doubleUnderscores进行了升级。标准委员会在将来使用它们,来为JS添加新特性,而不会危险到你已经存在代码。

什么时候使用 ES6 的标志符

标志符已经在 Firefox36 和 Chrome38 实现了。我自己已经在 Firefox 实现了,所以当你如果将symbol当作是cymbal时,你应该知道应该问谁了。

为了在未原生支持ES6 标志符的浏览器中使用标志符,你可以使用工具,如 core.js 。因为标志符实际上并不像以前的语言中的一样,这工具并不十分完美。请查看忠告

下周,我们将有两篇新的文章。首先,我们将会谈到一些长期的特性,也是在ES6中出现的–但受到抱怨的。我们将会涉及到两个特性,它们甚至是可以追溯到编程的源头。我们将会继续两个类似的特性,但只是点到为止。所以,请在下周加入我们,让我们深入ES6的集合中。

另外,让人迷恋的由 Gaston Silva 编写的文章谈到的并不是ES6的特性,但是,它也许在你项目的使用ES6上有一定的推动作用。下次见。

Comments