符号是通过工厂函数 Symbol()
创建的原始值
const mySymbol = Symbol('mySymbol');
参数是可选的,并提供描述,这主要用于调试。
符号是原始值
必须通过 typeof
对它们进行分类
const sym = Symbol();
.equal(typeof sym, 'symbol'); assert
它们可以是对象中的属性键
const obj = {
: 123,
[sym]; }
尽管符号是基元,但它们也类似于对象,因为 Symbol()
创建的每个值都是唯一的,并且不按值比较
> Symbol() === Symbol()false
在符号之前,如果我们需要唯一的值(仅等于自身),则对象是最佳选择
const string1 = 'abc';
const string2 = 'abc';
.equal(
assert=== string2, true); // not unique
string1
const object1 = {};
const object2 = {};
.equal(
assert=== object2, false); // unique
object1
const symbol1 = Symbol();
const symbol2 = Symbol();
.equal(
assert=== symbol2, false); // unique symbol1
我们传递给符号工厂函数的参数为创建的符号提供描述
const mySymbol = Symbol('mySymbol');
可以通过两种方式访问描述。
首先,它是 .toString()
返回的字符串的一部分
.equal(mySymbol.toString(), 'Symbol(mySymbol)'); assert
其次,自 ES2019 起,我们可以通过属性 .description
检索描述
.equal(mySymbol.description, 'mySymbol'); assert
符号的主要用例是
假设您要创建表示红色、橙色、黄色、绿色、蓝色和紫色的常量。一种简单的方法是使用字符串
const COLOR_BLUE = 'Blue';
从好的方面来说,记录该常量会产生有用的输出。从不好的方面来说,存在将不相关的值误认为颜色的风险,因为内容相同的两个字符串被认为是相等的
const MOOD_BLUE = 'Blue';
.equal(COLOR_BLUE, MOOD_BLUE); assert
我们可以通过符号解决这个问题
const COLOR_BLUE = Symbol('Blue');
const MOOD_BLUE = Symbol('Blue');
.notEqual(COLOR_BLUE, MOOD_BLUE); assert
让我们使用符号值常量来实现一个函数
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);
}
}.equal(getComplement(COLOR_YELLOW), COLOR_VIOLET); assert
对象中属性(字段)的键在两个级别使用
程序在基本级别运行。该级别的键反映了问题域(程序解决问题的领域),例如
ECMAScript 和许多库在元级别运行。它们管理数据并提供不属于问题域的服务,例如
标准方法 .toString()
由 ECMAScript 在创建对象的字符串表示形式时使用(第 A 行)
const point = {
x: 7,
y: 4,
toString() {
return `(${this.x}, ${this.y})`;
,
};
}.equal(
assertString(point), '(7, 4)'); // (A)
.x
和 .y
是基本级别的属性 - 它们用于解决使用点进行计算的问题。 .toString()
是元级别的属性 - 它与问题域无关。
标准 ECMAScript 方法 .toJSON()
const point = {
x: 7,
y: 4,
toJSON() {
return [this.x, this.y];
,
};
}.equal(
assertJSON.stringify(point), '[7,4]');
.x
和 .y
是基本级别的属性,.toJSON()
是元级别的属性。
程序的基本级别和元级别必须是独立的:基本级别的属性键不应与元级别的属性键冲突。
如果我们使用名称(字符串)作为属性键,我们将面临两个挑战
当一种语言首次创建时,它可以使用它想要的任何元级别名称。基本级别的代码被迫避免使用这些名称。然而,后来,当已经存在许多基本级别的代码时,就不能再自由选择元级别名称了。
我们可以引入命名规则来区分基本级别和元级别。例如,Python 使用两个下划线将元级别名称括起来:__init__
、__iter__
、__hash__
等。但是,语言的元级别名称和库的元级别名称仍然存在于同一个命名空间中,并且可能会发生冲突。
以下是后者对 JavaScript 造成问题的两个示例
2018 年 5 月,数组方法 .flatten()
必须重命名为 .flat()
,因为前一个名称已被库使用(来源)。
2020 年 11 月,数组方法 .item()
必须重命名为 .at()
,因为前一个名称已被库使用(来源)。
用作属性键的符号可以帮助我们解决这个问题:每个符号都是唯一的,并且符号键永远不会与任何其他字符串或符号键冲突。
例如,假设我们正在编写一个库,如果对象实现了特殊方法,则该库会以不同的方式处理它们。这就是为这种方法定义属性键并在对象中实现它的样子
const specialMethod = Symbol('specialMethod');
const obj = {
_id: 'kf12oi',
// (A)
[specialMethod]() { return this._id;
};
}.equal(obj[specialMethod](), 'kf12oi'); assert
第 A 行中的方括号使我们能够指定该方法必须具有键 specialMethod
。有关更多详细信息,请参阅§28.7.2 “对象字面量中的计算键”。
在 ECMAScript 中扮演特殊角色的符号称为公开已知的符号。示例包括
Symbol.iterator
:使对象可迭代。它是返回迭代器的方法的键。有关此主题的更多信息,请参阅§30 “同步迭代”。
Symbol.hasInstance
:自定义 instanceof
的工作方式。如果对象实现了具有该键的方法,则可以在该运算符的右侧使用它。例如
const PrimitiveNull = {
Symbol.hasInstance](x) {
[return x === null;
};
}.equal(null instanceof PrimitiveNull, true); assert
Symbol.toStringTag
:影响默认的 .toString()
方法。
> String({})'[object Object]'
> String({ [Symbol.toStringTag]: 'is no money' })'[object is no money]'
注意:通常最好覆盖 .toString()
。
练习:公开已知的符号
Symbol.toStringTag
:exercises/symbols/to_string_tag_test.mjs
Symbol.hasInstance
:exercises/symbols/has_instance_test.mjs
如果我们将符号 sym
转换为另一种原始类型,会发生什么情况?表 15 给出了答案。
转换为 | 显式转换 | 强制转换(隐式转换) |
---|---|---|
布尔值 | Boolean(sym) → 正常 |
!sym → 正常 |
数字 | Number(sym) → TypeError |
sym*2 → TypeError |
字符串 | String(sym) → 正常 |
''+sym → TypeError |
sym.toString() → 正常 |
`${sym}` → TypeError |
符号的一个关键陷阱是,在将它们转换为其他类型时,会抛出多少次异常。这背后的想法是什么?首先,转换为数字永远没有意义,应该发出警告。其次,将符号转换为字符串对于诊断输出确实很有用。但是,对意外地将符号转换为字符串(这是一种不同类型的属性键)发出警告也是有意义的
const obj = {};
const sym = Symbol();
.throws(
assert=> { obj['__'+sym+'__'] = true },
() message: 'Cannot convert a Symbol value to a string' }); {
缺点是异常使得使用符号更加复杂。通过加号运算符组合字符串时,必须显式转换符号
> const mySymbol = Symbol('mySymbol');
> 'Symbol I used: ' + mySymbolTypeError: Cannot convert a Symbol value to a string
> 'Symbol I used: ' + String(mySymbol)'Symbol I used: Symbol(mySymbol)'
测验
请参阅测验应用程序。