第七种类型
自从JavaScript在1997年首次标准化以来,已经有了六种类型。在ES6之前,JS程序中的每个值都属于这些类别之一:
- Undefined
- Null
- Boolean
- Number
- String
- Object
每种类型都是一组值。前五个集合都是有限的。当然,只有两个布尔值,true
和false
,而且它们不会产生新的值。有更多的Number和String值。该标准称,共有18,437,736,874,454,810,627个不同的数字(包括NaN
,即非数字的缩写)。与可能的字符串的数量相比,这简直是九牛一毛。
然而,Object值的集合是开放式的。每一件物品都是独一无二的、珍贵的雪花。每次打开Web页面时,都会创建大量新对象。
ES6 Symbols是值,但不是字符串。他们不是对象。它们是新的东西:第七种类型的值。让我们来谈谈它们可能会派上用场的情况。
一个简单的布尔值
有时,将一些额外的数据存储在真正属于其他人的JavaScript对象上是非常方便的。例如,假设您正在编写一个JS库,它使用CSS转换使DOM元素在屏幕上快速移动。你已经注意到,尝试在单个div上同时应用多个CSS过渡是行不通的。它会导致丑陋的、不连续的“跳跃”。你认为可以修复这个问题,但首先你需要一种方法来确定给定元素是否已经在移动。
这个问题该如何解决?
一种方法是使用CSS APIs询问浏览器元素是否在移动。但这听起来有点过分了。你的库应该已经知道元素在移动;这是一开始让它移动的代码!你真正需要的是一种跟踪哪些元素在移动的方法。你可以保存一个包含所有移动元素的数组。每次调用库动画元素时,都可以搜索数组,查看该元素是否已经存在。但是如果数组很大,线性搜索会很慢。
另一个办法是在元素上设置一个标志:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
这也存在一些潜在的问题。它们都与这样一个事实有关:你的代码并不是唯一使用DOM的代码。
- 其他使用
for-in
或Object.keys()
的代码可能会在你创建的属性上出错。 - 其他一些聪明的库作者可能首先想到了这种技术,因此你的库与现有库的交互会很糟糕。(属性名重复了,会有冲突)
- 其他一些聪明的库作者可能会在未来想到它,而你的js库与那个未来的库进行糟糕的交互。
- 标准委员会可能决定向所有元素添加
.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()
中添加一个带有该属性的元素时,将看到一个巨大的垃圾字符串。如果你需要不止一个这样的属性呢?你是如何让它们保持一致的?每次重新加载时,它们都会有不同的名称。
为什么这么难?我们只需要一个布尔值!
Symbols就是你要的答案
Symbols是程序可以创建并用作属性键的值,而不会有名称冲突的风险。
var mySymbol = Symbol();
调用Symbol()
将创建一个新的符号,该符号的值不等于任何其他值。
就像字符串或数字一样,可以使用符号作为属性键。因为它不等于任何字符串,所以这个符号键控属性保证不会与任何其他属性发生冲突。
obj[mySymbol] = "ok!"; // mySymbol是不会重复的属性名
console.log(obj[mySymbol]); // ok!
以下是在上面讨论的情况下如何使用符号:
// create a unique symbol
var isMoving = Symbol("isMoving");
...
if (element[isMoving]) {
smoothAnimations(element);
}
element[isMoving] = true;
关于这段代码的几点注意事项:
Symbol("isMoving")
中的字符串"isMoving"
被称为描述。这对调试很有帮助。当你将symbol写入console.log()
时,当使用.tostring()
将其转换为字符串时,以及可能在错误消息中都会显示它。这就是描述的用途。element[isMoving]
被称为符号键控属性(symbol-keyed property)。它只是一个名称是符号而不是字符串的属性。除此之外,它在任何方面都是正常的性质。- 与数组元素一样,符号键控属性不能使用点语法访问,如
obj.name
。必须使用方括号访问它们。 - 如果已经获得了符号键控属性,那么访问该符号键控属性是很简单的。上面的例子展示了如何获取和设置
element[isMoving]
,我们还可以询问if (isMoving in element)
,甚至如果需要的话可以删除delete element[isMoving]
。 - 另一方面,只要
isMoving
在作用域内,所有这些都是可能的。这使得Symbol成为一种弱封装机制:为自己创建一些Symbol的模块可以在任何它想要的对象上使用它们,而不必担心与其他代码创建的属性冲突。
因为符号键(symbol keys)是为了避免冲突而设计的,所以JavaScript最常见的对象检查特性就是简单地忽略符号键。例如,for-in
循环只在对象的字符串键上循环。跳过符号键。Object.keys(obj)
和Object.getOwnPropertyNames(obj)
做同样的事情。但是Symbol并不是完全私有的:可以使用新的APIObject. getownpropertysymbols(obj)
来列出对象的符号键。另一个新的APIReflect.ownKeys(obj)
同时返回字符串和符号键。
但到底什么是符号Symbols呢?
> typeof Symbol()
"symbol"
Symbols和其他东西不完全一样。
它们一旦被创造就不可改变。你不能在它们上设置属性(如果你在严格模式下尝试,你会得到一个TypeError)。它们可以是属性名。这些都是类似String的性质。
另一方面,每个Symbol都是独一无二的,不同于所有其他符号(甚至其他具有相同描述的符号),你可以轻松创建新的符号。这些都是类似Object的特性。
ES6 Symbol类似于Lisp和Ruby等语言中更传统的符号,但并没有紧密地集成到语言中。在Lisp中,所有标识符都是Symbols。在JS中,标识符和大多数属性键仍然被认为是字符串。Symbols只是一个额外的选择。
关于Symbol的一个快速警告:不像语言中的其他任何东西,它们不能自动转换为字符串。试图将符号与字符串进行转换将导致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()
。正如我们已经讨论过的,每次调用它都会返回一个新的唯一符号。 - 调用
Symbol.for(string)
。这将访问一组称为符号注册表(symbol registry)
的现有符号(symbol)。与Symbol()
定义的唯一符号不同,符号注册表中的符号是共享的。如果你调用Symbol.for("cat")
30次,它每次都会返回相同的符号。当多个网页或同一网页中的多个模块需要共享一个符号时,注册表是有用的。 - 使用由标准定义的符号,比如
Symbol.iterator
。一些符号是由标准本身定义的。每一个都有它自己的特殊目的。
Symbol的应用
Symbol.iterator
我们已经看到了ES6使用符号来避免与现有代码冲突的一种方法。在关于迭代器的文章中,我们看到for (var item of myArray)
的循环首先调用myArray[Symbol.iterator]()
。这个方法可以被称为myArray.iterator()
,但是symbol符号更利于代码向后兼容。
让instanceof可扩展
在ES6中,表达式 object instanceof constructor
被指定为构造函数constructor
的一个方法:constructor[Symbol.hasInstance](object)
。这意味着它是可扩展的。
消除新特性和旧代码之间的冲突
某些ES6 Array
方法仅仅出现在代码里就破坏了现有的网站。其他Web标准也有类似的问题:简单地在浏览器中添加新方法就会破坏现有的站点。然而,这种破坏主要是由所谓的动态作用域(dynamic scope)造成的,所以ES6引入了一种特殊的符号symbol.unscopables
, Web标准可以使用它来防止某些方法卷入动态作用域。
支持新的字符串匹配方式
在ES5中,str.match(myObject)
会试图把myObject
转换为RegExp
(正则表达式)。在ES6中,JS首先会检查myObject
是否有一个myObject[Symbol.match](str)
方法。现在JS库可以提供自定义字符串解析类,这些类可以在RegExp
对象工作的所有地方工作。
每一种用途都很小众。这些特性本身很难对我的日常代码产生重大影响。长远的观点更有趣。众所周知的符号是JavaScript在PHP和Python中__doubleUnderscores
下划线的改进版本。该标准将来将使用它们向语言中添加新的钩子,而不会对现有代码造成风险。