返回首页 深度解析 ECMAScript 6

符号

什么是 ES6 的符号?

符号不是图标。

他们不是你可以在你的程序中使用的小图片。

let = × ; // SyntaxError

他们不是代表别的东西的文学的工具。

他们是绝对是与钹不一样的东西。

Alt text

在编程中使用钹绝对不是一个好主意。他们有奔溃的趋势。

所以,符号到底是什么?

第七种类型

从 1997 年 JavaScript 第一次被标准化,已经出现了六种类型。直到 ES6,在 JS 程序的每个值都落入这些类别之一。

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

每种类型是一组值。前五个组都是有限的。当然,只有两个布尔值,真与假,而且他们不会制造新的。但是有更多的数值和字符串值。该标准说,其有 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-in 或者 Object.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;

下述是关于这个代码的一些笔记。

  • 在符号( "isMoving")中的字符串 "isMoving"被称为一个描述。它有助于代码调试。当你给 console.log( )写一个符号的时候,当你通过使用 .toString( )将其转化为一个字符串的时候,亦或者可能在错误信息中你可以感觉到它的好处。就是这样的。

  • element[isMoving] 被称为一个符号键属性。它只是一种名字是一个符号的属性,除此之外,它是在各方面都是一个正常属性。

  • 如数组元素相似,符号键属性不能像在 obj.name 中用点语法访问。他们必须使用方括号进行访问。

  • 如果你已经拿到了符号,访问符号键属性是微不足道的。上面的例子就是说明如何获取和设置 element[isMoving],而且我们还可以询问是否为 (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"

符号不像任何其他东西。

他们一旦创建就不可改变。你不能对它们进行属性设置(如果你在严格模式下尝试,你会得到一个类型错误)。它们可以是属性的名称。这些都是与字符串类似的性质。

在另一方面,每个符号都是独一无二的,与所有其他的符号都不相同(即使其他人有相同的描述)。同时,你可以很轻松地创建新的符号。这些是与对象相类似的性质。

ES6 符号类似于相对传统的语言中的 Lisp 和 Ruby,但其又没有非常紧密的集成于语言中。在 Lisp 中,所有标识符都是符号。在 JS 中,标识符和大部分属性关键字仍然被认为是字符串。符号只是一种额外的选择。

关于符号做个快速的警告:与几乎所有其他语言都不同的是,它们不能自动转换为字符串。试图用讲符号和字符串串联起来将导致一个类型错误。

> 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 使用了众所周知的符号。(这些功能都还没有在火狐浏览器中实现。)

  • 使得 instanceof 可扩展。在 ES6 中,语句 object instanceof constructor 被具体描述为构造的一个方法:constructor[Symbol.hasInstance](object)。这就意味着这是可扩展的。

  • 消除了新功能和旧代码之间的冲突。这真的很复杂,但是我们发现 ES6 的数组方法只是通过其存在性就使现有的网站奔溃了。其它的 Web 标准有类似的问题:在浏览器里简单地增加新的方法也会使现有的网站崩溃。然而,奔溃主要是由于一种叫做动态作用域的东西,所以 ES6 引入了一个特殊的符号,Symbol.unscopables,即 Web 标准可以用这个符号防止某些方法被卷入到动态作用域中。

  • 支持新类型的字符串匹配。在 ES5 中,str.match(myObject) 试图将 myObject 转换为一个正则表达式。在 ES6 中,它首先检查 myObject 是否有 myObject[Symbol.match](str) 的方法。现在的库可以提供自定义字符串解析类,其可以使用在任何可以使用正则表达式对象的地方。

上述的任意一个的用途都比较窄。很难从这些功能自身来看其对我们的代码产生的重大影响。从长远来看更有趣。著名的符号是 JavaScript 对 PHP 和 Python 的 __doubleUnderscores 的改进版本。该标准将在未来,在对你现有的代码不产生风险的前提下,使用他们在语言中添加新的挂钩,

我什么时候能使用 ES6 符号?

符号已经在火狐浏览器第 36 版本和谷歌浏览器第 38 版本中实现了。我自己也在火狐浏览器上进行了实现,所以如果你的符号曾经表现得像钹,你就会知道应该向谁倾诉。

为了支持那些对 ES6 符号还没有原生支持的浏览器,你可以使用一个 polyfill,如 core.js。由于符号与以前语言中的东西是完全不像的,polyfill 也不是完美的。请阅读注意事项

下章我们将讨论一些 JavaScript 期待已久且终于在 ES6 中到来的功能,并吐糟一下它不好的地方。我们将开始两个可以追溯到编程最初时光的功能。我们将继续讨论两个非常相似的但由 ephemerons 搭载的功能。

上一篇: 箭头函数 下一篇: 容器