ES6 深入 是一个系列,探讨 ECMAScript 标准第 6 版(简称 ES6)中新增的 JavaScript 编程语言的新特性。
注意:现在有一个 越南语 版本的这篇文章,由 Coupofy 团队 的 Julia Duong 创作。
ES6 符号是什么?
符号不是徽标。
它们不是您可以在代码中使用的图片。
let 😻 = 😍 x 😺; // SyntaxError
它们不是代表其他事物的文学手段。
它们绝对不是与铙钹相同的东西。
(在编程中使用铙钹不是一个好主意。它们很容易发生崩溃。)
那么,符号究竟是什么?
第七种类型
自从 JavaScript 在 1997 年首次标准化以来,已经出现了六种 类型。在 ES6 之前,JS 程序中的每个值都属于这些类别之一。
- 未定义
- 空
- 布尔值
- 数字
- 字符串
- 对象
每种类型都是一组值。前五个集合都是有限的。当然,只有两个布尔值,true
和 false
,而且它们不会创建新的布尔值。数字和字符串值的数量要多得多。标准规定有 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 的代码这一事实有关。
- 使用
for-in
或Object.keys()
的其他代码可能会遇到您创建的属性。 - 其他一些聪明的库作者可能已经想到过这种技术,您的库可能会与该现有库产生不良互动。
- 其他一些聪明的库作者将来可能会想到这种技术,您的库可能会与该未来的库产生不良互动。
- 标准委员会可能会决定向所有元素添加
.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 所需的推动。 那时见!
27 条评论