[ ]
和包装键访问属性Symbol.iterator
而不是 Symbol.ITERATOR
(等等)?Symbol
符号是 ECMAScript 6 中的一种新的原始类型。它们是通过工厂函数创建的
每次调用工厂函数时,都会创建一个新的唯一符号。可选参数是一个描述性字符串,在打印符号时显示(它没有其他用途)
符号主要用作唯一的属性键——符号永远不会与任何其他属性键(符号或字符串)冲突。例如,您可以通过使用存储在 Symbol.iterator
中的符号作为方法的键,使对象可迭代(可通过 for-of
循环和其他语言机制使用)(有关可迭代对象的更多信息,请参阅关于迭代的章节)
在 A 行中,符号用作方法的键。这个唯一的标记使对象可迭代,并使我们能够使用 for-of
循环。
在 ECMAScript 5 中,您可能使用字符串来表示颜色等概念。在 ES6 中,您可以使用符号,并确保它们始终是唯一的
每次调用 Symbol('Red')
时,都会创建一个新的符号。因此,COLOR_RED
永远不会被误认为是其他值。如果它是字符串 'Red'
,情况就会有所不同。
将符号强制(隐式转换)为字符串会引发异常
唯一的解决方案是显式转换
禁止强制转换可以防止一些错误,但也会使使用符号变得更加复杂。
以下操作识别符号作为属性键
Reflect.ownKeys()
[]
访问属性Object.assign()
以下操作忽略符号作为属性键
Object.keys()
Object.getOwnPropertyNames()
for-in
循环ECMAScript 6 引入了一种新的原始类型:符号。它们是用作唯一 ID 的标记。您可以通过工厂函数 Symbol()
创建符号(它与作为函数调用时返回字符串的 String
类似)
Symbol()
有一个可选的字符串值参数,允许您为新创建的符号提供描述。当符号转换为字符串时(通过 toString()
或 String()
),将使用该描述
Symbol()
返回的每个符号都是唯一的,每个符号都有自己的标识
如果将 typeof
运算符应用于其中一个符号,则可以看到符号是原始类型——它将返回一个新的特定于符号的结果
符号可以用作属性键
类和对象字面量有一个称为“计算属性键”的功能:您可以通过表达式指定属性的键,方法是将其放在方括号中。在以下对象字面量中,我们使用计算属性键使 MY_KEY
的值成为属性的键。
方法定义也可以有一个计算键
鉴于现在有一种新的值类型可以成为属性的键,因此 ECMAScript 6 使用以下术语
让我们通过首先创建一个对象来检查枚举自身属性键的 API。
Object.getOwnPropertyNames()
忽略符号值的属性键
Object.getOwnPropertySymbols()
忽略字符串值的属性键
Reflect.ownKeys()
考虑所有类型的键
Object.keys()
只考虑可枚举的字符串属性键
名称 Object.keys
与新术语冲突(只列出字符串键)。Object.names
或 Object.getEnumerableOwnPropertyNames
现在是更好的选择。
在 ECMAScript 5 中,通常使用字符串来表示概念(例如枚举常量)。例如
但是,字符串并不像我们希望的那样唯一。要了解原因,让我们看一下以下函数。
值得注意的是,您可以使用任意表达式作为 switch
的情况,没有任何限制。例如
我们利用 switch
为我们提供的灵活性,并通过我们的常量(COLOR_RED
等)来引用颜色,而不是硬编码它们('Red'
等)。
有趣的是,即使我们这样做,仍然可能出现混淆。例如,有人可能会为心情定义一个常量
现在,COLOR_BLUE
的值不再唯一,并且 MOOD_BLUE
可能会被误认为是它。如果将其用作 getComplement()
的参数,它将返回 'Orange'
,而它应该引发异常。
让我们使用符号来修复这个例子。现在我们也可以使用ES6 特性 const
,它允许我们声明实际的常量(您不能更改绑定到常量的值,但值本身可能是可变的)。
Symbol
返回的每个值都是唯一的,这就是为什么现在没有其他值可以被误认为是 BLUE
的原因。有趣的是,如果我们使用符号而不是字符串,getComplement()
的代码根本不会改变,这表明它们是多么相似。
能够创建其键永远不会与其他键冲突的属性在两种情况下很有用
每当 JavaScript 中存在继承层次结构时(例如,通过类、混入或纯原型方法创建),您都有两种属性
为了便于使用,公开属性通常具有字符串键。但是对于具有字符串键的私有属性,意外的名称冲突可能会成为一个问题。因此,符号是一个不错的选择。例如,在以下代码中,符号用于私有属性 _counter
和 _action
。
请注意,符号只能保护您免受名称冲突,而不能防止未经授权的访问,因为您可以通过 Reflect.ownKeys()
找出对象的 所有自身属性键,包括符号。如果您也希望在那里得到保护,可以使用“类的私有数据”一节中列出的方法之一。
符号具有唯一的标识,这使得它们非常适合作为存在于与“普通”属性键不同级别的公开属性的键,因为元级键和普通键不能冲突。元级属性的一个例子是对象可以实现的方法,用于自定义库如何处理它们。使用符号键可以防止库将普通方法误认为是自定义方法。
ES6 “可迭代性”就是这样一种自定义。如果一个对象有一个方法,其键是符号(存储在 Symbol.iterator
中),则该对象是“可迭代的”。在以下代码中,obj
是可迭代的。
obj
的可迭代性使您能够使用 for-of
循环和类似的 JavaScript 特性
如果您认为名称冲突无关紧要,以下三个例子说明了名称冲突在 JavaScript 标准库的演变过程中是如何导致问题的
Array.prototype.values()
时,它破坏了使用 with
和数组的现有代码,并遮蔽了外部作用域中的变量 values
(错误报告 1,错误报告 2)。因此,引入了一种机制来隐藏 with
的属性(Symbol.unscopables
)。String.prototype.contains
与 MooTools 添加的方法冲突,不得不重命名为 String.prototype.includes
(错误报告)。Array.prototype.contains
也与 MooTools 添加的方法冲突,不得不重命名为 Array.prototype.includes
(错误报告)。相反,通过属性键 Symbol.iterator
向对象添加可迭代性不会导致问题,因为该键不会与任何内容冲突。
下表显示了将符号显式或隐式转换为其他原始类型时会发生什么
转换为 | 显式转换 | 强制转换(隐式转换) |
---|---|---|
布尔值 |
Boolean(sym) → 正常 |
!sym → 正常 |
数字 |
Number(sym) → TypeError |
sym*2 → TypeError |
字符串 |
String(sym) → 正常 |
''+sym → TypeError |
sym.toString() → 正常 |
`${sym}` → TypeError |
禁止强制转换为字符串很容易让你出错。
要解决这些问题,你需要显式地转换为字符串。
对于符号,通常禁止强制转换(隐式转换)。本节解释了原因。
始终允许强制转换为布尔值,主要目的是在 if
语句和其他位置启用真值检查。
符号是特殊的属性键,这就是为什么你要避免意外地将它们转换为字符串,字符串是另一种属性键。如果你使用加法运算符来计算属性的名称,就可能发生这种情况。
这就是为什么如果 value
是符号,就会抛出 TypeError
。
你也不希望意外地将符号转换为数组索引。如果 value
是符号,则以下代码可能会发生这种情况。
这就是为什么在这种情况下加法运算符会抛出错误。
要将符号显式转换为布尔值,可以调用 Boolean()
,它对符号返回 true
。
Boolean()
通过内部操作 ToBoolean()
计算其结果,该操作对符号和其他真值返回 true
。
强制转换也使用 ToBoolean()
。
要将符号显式转换为数字,可以调用 Number()
。
Number()
通过内部操作 ToNumber()
计算其结果,该操作对符号抛出 TypeError
。
强制转换也使用 ToNumber()
。
要将符号显式转换为字符串,可以调用 String()
。
如果 String()
的参数是符号,则它会自行处理到字符串的转换,并返回用字符串 Symbol()
包裹的、在创建符号时提供的描述。如果没有给出描述,则使用空字符串。
toString()
方法返回与 String()
相同的字符串,但这两种操作都不会调用对方,它们都调用相同的内部操作 SymbolDescriptiveString()
。
强制转换通过内部操作 ToString()
处理,该操作对符号抛出 TypeError
。一种将参数强制转换为字符串的方法是 Number.parseInt()
。
+
) 进行转换 加法运算符 的工作原理如下:
ToString()
),将它们连接起来并返回结果。强制转换为字符串或数字都会抛出异常,这意味着你不能(直接)对符号使用加法运算符。
虽然所有其他原始值都有字面量,但你需要通过函数调用 Symbol
来创建符号。因此,存在意外地将 Symbol
作为构造函数调用的风险。这会产生 Symbol
的实例,而这些实例并不是很有用。因此,当你尝试这样做时,会抛出一个异常。
仍然有一种方法可以创建包装对象,即 Symbol
的实例:Object
,作为函数调用时,会将所有值转换为对象,包括符号。
[ ]
和包装键访问属性 方括号运算符 [ ]
通常将其操作数强制转换为字符串。现在有两个例外:符号包装对象被解包,符号按原样使用。让我们使用以下对象来研究这种现象。
方括号运算符会解包包装的符号。
与任何其他与符号无关的值一样,包装的字符串也会被方括号运算符转换为字符串。
用于获取和设置属性的运算符使用内部操作 ToPropertyKey()
,其工作原理如下:
ToPrimitive()
将操作数转换为原始值,首选类型为 String
。[@@toPrimitive]()
,则使用该方法将其转换为原始值。符号有这样一个方法,它返回包装的符号。toString()
方法返回一个原始值,则通过 toString()
将其转换为原始值。否则,如果 valueOf()
方法返回一个原始值,则使用 valueOf()
方法。否则,抛出一个 TypeError
。首选类型 String
决定了先调用 toString()
,再调用 valueOf()
。ToString()
将结果强制转换为字符串。代码领域(简称:领域)是代码片段存在的上下文。它包括全局变量、加载的模块等等。即使代码存在于“单个”领域“内部”,它也可以访问其他领域中的代码。例如,浏览器中的每个框架都有自己的领域。执行可以从一个框架跳转到另一个框架,如下面的 HTML 所示。
问题是每个领域都有自己的全局变量,其中每个变量 Array
都指向不同的对象,即使它们本质上都是同一个对象。类似地,库和用户代码在每个领域中只加载一次,并且每个领域都有同一个对象的版本。
对象是通过标识进行比较的,而布尔值、数字和字符串是通过值进行比较的。因此,无论数字 123 来自哪个领域,它都与所有其他 123 没有区别。这类似于数字字面量 123
总是产生相同的值。
符号具有独立的标识,因此不能像其他原始值那样顺利地跨领域传递。对于像 Symbol.iterator
这样应该跨领域工作的符号来说,这是一个问题:如果一个对象在一个领域中是可迭代的,那么它在所有领域中都应该是可迭代的。所有内置符号都由 JavaScript 引擎管理,这确保了例如 Symbol.iterator
在每个领域中都是相同的值。如果一个库想要提供跨领域符号,它必须依赖额外的支持,这种支持以全局符号注册表的形式出现:这个注册表对所有领域都是全局的,并将字符串映射到符号。对于每个符号,库都需要提供一个尽可能唯一的字符串。要创建符号,它不使用 Symbol()
,而是向注册表请求字符串映射到的符号。如果注册表中已经存在该字符串的条目,则返回关联的符号。否则,将首先创建条目和符号。
你可以通过 Symbol.for()
向注册表请求符号,并通过 Symbol.keyFor()
检索与符号关联的字符串(其键)。
由 JavaScript 引擎提供的跨领域符号(例如 Symbol.iterator
)不在注册表中。
最初的计划是使用符号来支持私有属性(本来会有公共符号和私有符号)。但该特性被放弃了,因为使用“get”和“set”(两个元对象协议操作)来管理私有数据与代理的交互不好。
这两个目标是矛盾的。关于类的章节解释了管理私有数据的选项。符号是其中一种选择,但你无法获得与私有符号相同的安全性,因为可以通过 Object.getOwnPropertySymbols()
和 Reflect.ownKeys()
确定用作对象属性键的符号。
在某些方面,符号类似于原始值,而在其他方面,它们类似于对象。
那么符号是什么——原始值还是对象?最终,它们被变成了原始值,原因有两个。
首先,符号更像字符串而不是对象:它们是语言的基本值,它们是不可变的,并且可以用作属性键。符号具有唯一标识并不一定与它们像字符串相矛盾:UUID 算法生成的字符串是准唯一的。
其次,符号最常被用作属性键,因此优化 JavaScript 规范和实现以适应这种用例是有意义的。这样一来,符号就不需要对象的许多功能了。
instanceof
、Object.keys()
等。符号不具备这些功能,这使得规范和实现变得更容易。V8 团队还表示,在属性键方面,将原始类型作为特例比某些对象更容易处理。
与字符串相比,符号是唯一的,可以防止名称冲突。这对于颜色之类的标记来说很好,但对于支持元级别方法(例如键为 Symbol.iterator
的方法)来说至关重要。Python 使用特殊名称 __iter__
来避免冲突。您可以保留双下划线名称用于编程语言机制,但库该怎么办?使用符号,我们有了一种适用于所有人的可扩展性机制。正如您稍后将在公共符号部分中看到的那样,JavaScript 本身已经充分利用了这种机制。
对于无冲突的属性键,除了符号之外,还有一种假设的替代方法:使用命名约定。例如,带有 URL 的字符串(例如 'http://example.com/iterator'
)。但这会引入第二类属性键(相对于通常是有效标识符并且不包含冒号、斜杠、点等的“普通”属性名称),这基本上就是符号的用途。那么我们不妨引入一种新的值类型。
不,它们不一样。
Ruby 的符号基本上是用于创建值的字面量。两次提及同一个符号会产生两次相同的值。
JavaScript 函数 Symbol()
是一个符号工厂——它返回的每个值都是唯一的。
Symbol.iterator
而不是 Symbol.ITERATOR
(等等)? 知名符号存储在名称以小写字符开头并采用驼峰式命名的属性中。在某种程度上,这些属性是常量,习惯上常量使用全大写名称(Math.PI
等)。但它们拼写的理由不同:知名符号用于代替普通属性键,这就是为什么它们的“名称”遵循属性键的规则,而不是常量的规则。
本节概述了 ECMAScript 6 符号 API。
Symbol
Symbol(description?) : symbol
创建一个新符号。可选参数 description
允许您为符号提供描述。访问描述的唯一方法是将符号转换为字符串(通过 toString()
或 String()
)。这种转换的结果是 'Symbol('+description+')'
。
Symbol
不能用作构造函数——如果您通过 new
调用它,则会抛出异常。
符号唯一有用的方法是 toString()
(通过 Symbol.prototype.toString()
)。
转换为 | 显式转换 | 强制转换(隐式转换) |
---|---|---|
布尔值 |
Boolean(sym) → 正常 |
!sym → 正常 |
数字 |
Number(sym) → TypeError |
sym*2 → TypeError |
字符串 |
String(sym) → 正常 |
''+sym → TypeError |
sym.toString() → 正常 |
`${sym}` → TypeError |
|
对象 |
Object(sym) → OK |
Object.keys(sym) → OK |
全局对象 Symbol
有几个属性,它们充当所谓的_知名符号_的常量。这些符号允许您通过将它们用作属性键来配置 ES6 如何处理对象。这是所有知名符号的列表。
Symbol.hasInstance
(方法)C
自定义 x instanceof C
的行为。Symbol.toPrimitive
(方法)Symbol.toStringTag
(字符串)Object.prototype.toString()
调用以计算对象 obj
的默认字符串描述。Symbol.unscopables
(对象)with
语句隐藏某些属性。Symbol.iterator
(方法)for-of
循环和扩展运算符 (...
))。该方法返回一个_迭代器_。详细信息:章节“可迭代对象和迭代器”。String.prototype.match(x, ···)
被转发到 x[Symbol.match](···)
。String.prototype.replace(x, ···)
被转发到 x[Symbol.replace](···)
。String.prototype.search(x, ···)
被转发到 x[Symbol.search](···)
。String.prototype.split(x, ···)
被转发到 x[Symbol.split](···)
。详细信息在字符串章节的“将正则表达式工作委托给其参数的字符串方法”一节中解释。
Symbol.species
(方法)Array.prototype.map()
)如何创建类似于 this
的对象。详细信息在关于类的章节中解释。Symbol.isConcatSpreadable
(布尔值)Array.prototype.concat()
是将其结果的索引元素添加到其结果中(“扩展”)还是将对象作为单个元素添加(详细信息在关于数组的章节中解释)。如果您希望符号在所有领域中都相同,则需要通过以下两种方法使用全局符号注册表:
Symbol.for(str) : symbol
str
的符号。如果注册表中尚不存在 str
,则会创建一个新符号并将其归档在注册表中,键为 str
。Symbol.keyFor(sym) : string
sym
关联的字符串。如果注册表中不存在 sym
,则此方法返回 undefined
。此方法可用于序列化符号(例如,转换为 JSON)。