[ ] 和包装键访问属性Symbol.iterator 而不是 Symbol.ITERATOR(等等)?Symbol符号是 ECMAScript 6 中的一种新的原始类型。它们是通过工厂函数创建的
const mySymbol = Symbol('mySymbol');
每次调用工厂函数时,都会创建一个新的唯一符号。可选参数是一个描述性字符串,在打印符号时显示(它没有其他用途)
> mySymbol
Symbol(mySymbol)
符号主要用作唯一的属性键——符号永远不会与任何其他属性键(符号或字符串)冲突。例如,您可以通过使用存储在 Symbol.iterator 中的符号作为方法的键,使对象可迭代(可通过 for-of 循环和其他语言机制使用)(有关可迭代对象的更多信息,请参阅关于迭代的章节)
const iterableObject = {
[Symbol.iterator]() { // (A)
···
}
}
for (const x of iterableObject) {
console.log(x);
}
// Output:
// hello
// world
在 A 行中,符号用作方法的键。这个唯一的标记使对象可迭代,并使我们能够使用 for-of 循环。
在 ECMAScript 5 中,您可能使用字符串来表示颜色等概念。在 ES6 中,您可以使用符号,并确保它们始终是唯一的
const COLOR_RED = Symbol('Red');
const COLOR_ORANGE = Symbol('Orange');
const COLOR_YELLOW = Symbol('Yellow');
const COLOR_GREEN = Symbol('Green');
const COLOR_BLUE = Symbol('Blue');
const COLOR_VIOLET = Symbol('Violet');
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_ORANGE:
return COLOR_BLUE;
case COLOR_YELLOW:
return COLOR_VIOLET;
case COLOR_GREEN:
return COLOR_RED;
case COLOR_BLUE:
return COLOR_ORANGE;
case COLOR_VIOLET:
return COLOR_YELLOW;
default:
throw new Exception('Unknown color: '+color);
}
}
每次调用 Symbol('Red') 时,都会创建一个新的符号。因此,COLOR_RED 永远不会被误认为是其他值。如果它是字符串 'Red',情况就会有所不同。
将符号强制(隐式转换)为字符串会引发异常
const sym = Symbol('desc');
const str1 = '' + sym; // TypeError
const str2 = `${sym}`; // TypeError
唯一的解决方案是显式转换
const str2 = String(sym); // 'Symbol(desc)'
const str3 = sym.toString(); // 'Symbol(desc)'
禁止强制转换可以防止一些错误,但也会使使用符号变得更加复杂。
以下操作识别符号作为属性键
Reflect.ownKeys()[] 访问属性Object.assign()以下操作忽略符号作为属性键
Object.keys()Object.getOwnPropertyNames()for-in 循环ECMAScript 6 引入了一种新的原始类型:符号。它们是用作唯一 ID 的标记。您可以通过工厂函数 Symbol() 创建符号(它与作为函数调用时返回字符串的 String 类似)
const symbol1 = Symbol();
Symbol() 有一个可选的字符串值参数,允许您为新创建的符号提供描述。当符号转换为字符串时(通过 toString() 或 String()),将使用该描述
> const symbol2 = Symbol('symbol2');
> String(symbol2)
'Symbol(symbol2)'
Symbol() 返回的每个符号都是唯一的,每个符号都有自己的标识
> Symbol() === Symbol()
false
如果将 typeof 运算符应用于其中一个符号,则可以看到符号是原始类型——它将返回一个新的特定于符号的结果
> typeof Symbol()
'symbol'
符号可以用作属性键
const MY_KEY = Symbol();
const obj = {};
obj[MY_KEY] = 123;
console.log(obj[MY_KEY]); // 123
类和对象字面量有一个称为“计算属性键”的功能:您可以通过表达式指定属性的键,方法是将其放在方括号中。在以下对象字面量中,我们使用计算属性键使 MY_KEY 的值成为属性的键。
const MY_KEY = Symbol();
const obj = {
[MY_KEY]: 123
};
方法定义也可以有一个计算键
const FOO = Symbol();
const obj = {
[FOO]() {
return 'bar';
}
};
console.log(obj[FOO]()); // bar
鉴于现在有一种新的值类型可以成为属性的键,因此 ECMAScript 6 使用以下术语
让我们通过首先创建一个对象来检查枚举自身属性键的 API。
const obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Object.defineProperty(obj,
'nonEnum', { enumerable: false });
Object.getOwnPropertyNames() 忽略符号值的属性键
> Object.getOwnPropertyNames(obj)
['enum', 'nonEnum']
Object.getOwnPropertySymbols() 忽略字符串值的属性键
> Object.getOwnPropertySymbols(obj)
[Symbol(my_key)]
Reflect.ownKeys() 考虑所有类型的键
> Reflect.ownKeys(obj)
[Symbol(my_key), 'enum', 'nonEnum']
Object.keys() 只考虑可枚举的字符串属性键
> Object.keys(obj)
['enum']
名称 Object.keys 与新术语冲突(只列出字符串键)。Object.names 或 Object.getEnumerableOwnPropertyNames 现在是更好的选择。
在 ECMAScript 5 中,通常使用字符串来表示概念(例如枚举常量)。例如
var COLOR_RED = 'Red';
var COLOR_ORANGE = 'Orange';
var COLOR_YELLOW = 'Yellow';
var COLOR_GREEN = 'Green';
var COLOR_BLUE = 'Blue';
var COLOR_VIOLET = 'Violet';
但是,字符串并不像我们希望的那样唯一。要了解原因,让我们看一下以下函数。
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_ORANGE:
return COLOR_BLUE;
case COLOR_YELLOW:
return COLOR_VIOLET;
case COLOR_GREEN:
return COLOR_RED;
case COLOR_BLUE:
return COLOR_ORANGE;
case COLOR_VIOLET:
return COLOR_YELLOW;
default:
throw new Exception('Unknown color: '+color);
}
}
值得注意的是,您可以使用任意表达式作为 switch 的情况,没有任何限制。例如
function isThree(x) {
switch (x) {
case 1 + 1 + 1:
return true;
default:
return false;
}
}
我们利用 switch 为我们提供的灵活性,并通过我们的常量(COLOR_RED 等)来引用颜色,而不是硬编码它们('Red' 等)。
有趣的是,即使我们这样做,仍然可能出现混淆。例如,有人可能会为心情定义一个常量
var MOOD_BLUE = 'Blue';
现在,COLOR_BLUE 的值不再唯一,并且 MOOD_BLUE 可能会被误认为是它。如果将其用作 getComplement() 的参数,它将返回 'Orange',而它应该引发异常。
让我们使用符号来修复这个例子。现在我们也可以使用ES6 特性 const,它允许我们声明实际的常量(您不能更改绑定到常量的值,但值本身可能是可变的)。
const COLOR_RED = Symbol('Red');
const COLOR_ORANGE = Symbol('Orange');
const COLOR_YELLOW = Symbol('Yellow');
const COLOR_GREEN = Symbol('Green');
const COLOR_BLUE = Symbol('Blue');
const COLOR_VIOLET = Symbol('Violet');
Symbol 返回的每个值都是唯一的,这就是为什么现在没有其他值可以被误认为是 BLUE 的原因。有趣的是,如果我们使用符号而不是字符串,getComplement() 的代码根本不会改变,这表明它们是多么相似。
能够创建其键永远不会与其他键冲突的属性在两种情况下很有用
每当 JavaScript 中存在继承层次结构时(例如,通过类、混入或纯原型方法创建),您都有两种属性
为了便于使用,公开属性通常具有字符串键。但是对于具有字符串键的私有属性,意外的名称冲突可能会成为一个问题。因此,符号是一个不错的选择。例如,在以下代码中,符号用于私有属性 _counter 和 _action。
const _counter = Symbol('counter');
const _action = Symbol('action');
class Countdown {
constructor(counter, action) {
this[_counter] = counter;
this[_action] = action;
}
dec() {
let counter = this[_counter];
if (counter < 1) return;
counter--;
this[_counter] = counter;
if (counter === 0) {
this[_action]();
}
}
}
请注意,符号只能保护您免受名称冲突,而不能防止未经授权的访问,因为您可以通过 Reflect.ownKeys() 找出对象的 所有自身属性键,包括符号。如果您也希望在那里得到保护,可以使用“类的私有数据”一节中列出的方法之一。
符号具有唯一的标识,这使得它们非常适合作为存在于与“普通”属性键不同级别的公开属性的键,因为元级键和普通键不能冲突。元级属性的一个例子是对象可以实现的方法,用于自定义库如何处理它们。使用符号键可以防止库将普通方法误认为是自定义方法。
ES6 “可迭代性”就是这样一种自定义。如果一个对象有一个方法,其键是符号(存储在 Symbol.iterator 中),则该对象是“可迭代的”。在以下代码中,obj 是可迭代的。
const obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
···
}
};
obj 的可迭代性使您能够使用 for-of 循环和类似的 JavaScript 特性
for (const x of obj) {
console.log(x);
}
// Output:
// hello
// world
如果您认为名称冲突无关紧要,以下三个例子说明了名称冲突在 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 |
禁止强制转换为字符串很容易让你出错。
const sym = Symbol();
console.log('A symbol: '+sym); // TypeError
console.log(`A symbol: ${sym}`); // TypeError
要解决这些问题,你需要显式地转换为字符串。
console.log('A symbol: '+String(sym)); // OK
console.log(`A symbol: ${String(sym)}`); // OK
对于符号,通常禁止强制转换(隐式转换)。本节解释了原因。
始终允许强制转换为布尔值,主要目的是在 if 语句和其他位置启用真值检查。
if (value) { ··· }
param = param || 0;
符号是特殊的属性键,这就是为什么你要避免意外地将它们转换为字符串,字符串是另一种属性键。如果你使用加法运算符来计算属性的名称,就可能发生这种情况。
myObject['__' + value]
这就是为什么如果 value 是符号,就会抛出 TypeError。
你也不希望意外地将符号转换为数组索引。如果 value 是符号,则以下代码可能会发生这种情况。
myArray[1 + value]
这就是为什么在这种情况下加法运算符会抛出错误。
要将符号显式转换为布尔值,可以调用 Boolean(),它对符号返回 true。
> const sym = Symbol('hello');
> Boolean(sym)
true
Boolean() 通过内部操作 ToBoolean() 计算其结果,该操作对符号和其他真值返回 true。
强制转换也使用 ToBoolean()。
> !sym
false
要将符号显式转换为数字,可以调用 Number()。
> const sym = Symbol('hello');
> Number(sym)
TypeError: can't convert symbol to number
Number() 通过内部操作 ToNumber() 计算其结果,该操作对符号抛出 TypeError。
强制转换也使用 ToNumber()。
> +sym
TypeError: can't convert symbol to number
要将符号显式转换为字符串,可以调用 String()。
> const sym = Symbol('hello');
> String(sym)
'Symbol(hello)'
如果 String() 的参数是符号,则它会自行处理到字符串的转换,并返回用字符串 Symbol() 包裹的、在创建符号时提供的描述。如果没有给出描述,则使用空字符串。
> String(Symbol())
'Symbol()'
toString() 方法返回与 String() 相同的字符串,但这两种操作都不会调用对方,它们都调用相同的内部操作 SymbolDescriptiveString()。
> Symbol('hello').toString()
'Symbol(hello)'
强制转换通过内部操作 ToString() 处理,该操作对符号抛出 TypeError。一种将参数强制转换为字符串的方法是 Number.parseInt()。
> Number.parseInt(Symbol())
TypeError: can't convert symbol to string
+) 进行转换 加法运算符 的工作原理如下:
ToString()),将它们连接起来并返回结果。强制转换为字符串或数字都会抛出异常,这意味着你不能(直接)对符号使用加法运算符。
> '' + Symbol()
TypeError: can't convert symbol to string
> 1 + Symbol()
TypeError: can't convert symbol to number
虽然所有其他原始值都有字面量,但你需要通过函数调用 Symbol 来创建符号。因此,存在意外地将 Symbol 作为构造函数调用的风险。这会产生 Symbol 的实例,而这些实例并不是很有用。因此,当你尝试这样做时,会抛出一个异常。
> new Symbol()
TypeError: Symbol is not a constructor
仍然有一种方法可以创建包装对象,即 Symbol 的实例:Object,作为函数调用时,会将所有值转换为对象,包括符号。
> const sym = Symbol();
> typeof sym
'symbol'
> const wrapper = Object(sym);
> typeof wrapper
'object'
> wrapper instanceof Symbol
true
[ ] 和包装键访问属性 方括号运算符 [ ] 通常将其操作数强制转换为字符串。现在有两个例外:符号包装对象被解包,符号按原样使用。让我们使用以下对象来研究这种现象。
const sym = Symbol('yes');
const obj = {
[sym]: 'a',
str: 'b',
};
方括号运算符会解包包装的符号。
> const wrappedSymbol = Object(sym);
> typeof wrappedSymbol
'object'
> obj[wrappedSymbol]
'a'
与任何其他与符号无关的值一样,包装的字符串也会被方括号运算符转换为字符串。
> const wrappedString = new String('str');
> typeof wrappedString
'object'
> obj[wrappedString]
'b'
用于获取和设置属性的运算符使用内部操作 ToPropertyKey(),其工作原理如下:
ToPrimitive() 将操作数转换为原始值,首选类型为 String。[@@toPrimitive](),则使用该方法将其转换为原始值。符号有这样一个方法,它返回包装的符号。toString() 方法返回一个原始值,则通过 toString() 将其转换为原始值。否则,如果 valueOf() 方法返回一个原始值,则使用 valueOf() 方法。否则,抛出一个 TypeError。首选类型 String 决定了先调用 toString(),再调用 valueOf()。ToString() 将结果强制转换为字符串。代码领域(简称:领域)是代码片段存在的上下文。它包括全局变量、加载的模块等等。即使代码存在于“单个”领域“内部”,它也可以访问其他领域中的代码。例如,浏览器中的每个框架都有自己的领域。执行可以从一个框架跳转到另一个框架,如下面的 HTML 所示。
<head>
<script>
function test(arr) {
var iframe = frames[0];
// This code and the iframe’s code exist in
// different realms. Therefore, global variables
// such as Array are different:
console.log(Array === iframe.Array); // false
console.log(arr instanceof Array); // false
console.log(arr instanceof iframe.Array); // true
// But: symbols are the same
console.log(Symbol.iterator ===
iframe.Symbol.iterator); // true
}
</script>
</head>
<body>
<iframe srcdoc="<script>window.parent.test([])</script>">
</iframe>
</body>
问题是每个领域都有自己的全局变量,其中每个变量 Array 都指向不同的对象,即使它们本质上都是同一个对象。类似地,库和用户代码在每个领域中只加载一次,并且每个领域都有同一个对象的版本。
对象是通过标识进行比较的,而布尔值、数字和字符串是通过值进行比较的。因此,无论数字 123 来自哪个领域,它都与所有其他 123 没有区别。这类似于数字字面量 123 总是产生相同的值。
符号具有独立的标识,因此不能像其他原始值那样顺利地跨领域传递。对于像 Symbol.iterator 这样应该跨领域工作的符号来说,这是一个问题:如果一个对象在一个领域中是可迭代的,那么它在所有领域中都应该是可迭代的。所有内置符号都由 JavaScript 引擎管理,这确保了例如 Symbol.iterator 在每个领域中都是相同的值。如果一个库想要提供跨领域符号,它必须依赖额外的支持,这种支持以全局符号注册表的形式出现:这个注册表对所有领域都是全局的,并将字符串映射到符号。对于每个符号,库都需要提供一个尽可能唯一的字符串。要创建符号,它不使用 Symbol(),而是向注册表请求字符串映射到的符号。如果注册表中已经存在该字符串的条目,则返回关联的符号。否则,将首先创建条目和符号。
你可以通过 Symbol.for() 向注册表请求符号,并通过 Symbol.keyFor() 检索与符号关联的字符串(其键)。
> const sym = Symbol.for('Hello everybody!');
> Symbol.keyFor(sym)
'Hello everybody!'
由 JavaScript 引擎提供的跨领域符号(例如 Symbol.iterator)不在注册表中。
> Symbol.keyFor(Symbol.iterator)
undefined
最初的计划是使用符号来支持私有属性(本来会有公共符号和私有符号)。但该特性被放弃了,因为使用“get”和“set”(两个元对象协议操作)来管理私有数据与代理的交互不好。
这两个目标是矛盾的。关于类的章节解释了管理私有数据的选项。符号是其中一种选择,但你无法获得与私有符号相同的安全性,因为可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 确定用作对象属性键的符号。
在某些方面,符号类似于原始值,而在其他方面,它们类似于对象。
那么符号是什么——原始值还是对象?最终,它们被变成了原始值,原因有两个。
首先,符号更像字符串而不是对象:它们是语言的基本值,它们是不可变的,并且可以用作属性键。符号具有唯一标识并不一定与它们像字符串相矛盾:UUID 算法生成的字符串是准唯一的。
其次,符号最常被用作属性键,因此优化 JavaScript 规范和实现以适应这种用例是有意义的。这样一来,符号就不需要对象的许多功能了。
instanceof、Object.keys() 等。符号不具备这些功能,这使得规范和实现变得更容易。V8 团队还表示,在属性键方面,将原始类型作为特例比某些对象更容易处理。
与字符串相比,符号是唯一的,可以防止名称冲突。这对于颜色之类的标记来说很好,但对于支持元级别方法(例如键为 Symbol.iterator 的方法)来说至关重要。Python 使用特殊名称 __iter__ 来避免冲突。您可以保留双下划线名称用于编程语言机制,但库该怎么办?使用符号,我们有了一种适用于所有人的可扩展性机制。正如您稍后将在公共符号部分中看到的那样,JavaScript 本身已经充分利用了这种机制。
对于无冲突的属性键,除了符号之外,还有一种假设的替代方法:使用命名约定。例如,带有 URL 的字符串(例如 'http://example.com/iterator')。但这会引入第二类属性键(相对于通常是有效标识符并且不包含冒号、斜杠、点等的“普通”属性名称),这基本上就是符号的用途。那么我们不妨引入一种新的值类型。
不,它们不一样。
Ruby 的符号基本上是用于创建值的字面量。两次提及同一个符号会产生两次相同的值。
:foo == :foo
JavaScript 函数 Symbol() 是一个符号工厂——它返回的每个值都是唯一的。
Symbol('foo') !== Symbol('foo')
Symbol.iterator 而不是 Symbol.ITERATOR(等等)? 知名符号存储在名称以小写字符开头并采用驼峰式命名的属性中。在某种程度上,这些属性是常量,习惯上常量使用全大写名称(Math.PI 等)。但它们拼写的理由不同:知名符号用于代替普通属性键,这就是为什么它们的“名称”遵循属性键的规则,而不是常量的规则。
本节概述了 ECMAScript 6 符号 API。
Symbol Symbol(description?) : symbol
创建一个新符号。可选参数 description 允许您为符号提供描述。访问描述的唯一方法是将符号转换为字符串(通过 toString() 或 String())。这种转换的结果是 'Symbol('+description+')'。
> const sym = Symbol('hello');
> String(sym)
'Symbol(hello)'
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 的默认字符串描述。 '[object ' + obj[Symbol.toStringTag] + ']'
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) : symbolstr 的符号。如果注册表中尚不存在 str,则会创建一个新符号并将其归档在注册表中,键为 str。Symbol.keyFor(sym) : stringsym 关联的字符串。如果注册表中不存在 sym,则此方法返回 undefined。此方法可用于序列化符号(例如,转换为 JSON)。