符号是通过工厂函数 Symbol() 创建的原始值
const mySymbol = Symbol('mySymbol');参数是可选的,并提供描述,这主要用于调试。
符号是原始值
必须通过 typeof 对它们进行分类
const sym = Symbol();
assert.equal(typeof sym, 'symbol');它们可以是对象中的属性键
const obj = {
[sym]: 123,
};尽管符号是基元,但它们也类似于对象,因为 Symbol() 创建的每个值都是唯一的,并且不按值比较
> Symbol() === Symbol()
false在符号之前,如果我们需要唯一的值(仅等于自身),则对象是最佳选择
const string1 = 'abc';
const string2 = 'abc';
assert.equal(
string1 === string2, true); // not unique
const object1 = {};
const object2 = {};
assert.equal(
object1 === object2, false); // unique
const symbol1 = Symbol();
const symbol2 = Symbol();
assert.equal(
symbol1 === symbol2, false); // unique我们传递给符号工厂函数的参数为创建的符号提供描述
const mySymbol = Symbol('mySymbol');可以通过两种方式访问描述。
首先,它是 .toString() 返回的字符串的一部分
assert.equal(mySymbol.toString(), 'Symbol(mySymbol)');其次,自 ES2019 起,我们可以通过属性 .description 检索描述
assert.equal(mySymbol.description, 'mySymbol');符号的主要用例是
假设您要创建表示红色、橙色、黄色、绿色、蓝色和紫色的常量。一种简单的方法是使用字符串
const COLOR_BLUE = 'Blue';从好的方面来说,记录该常量会产生有用的输出。从不好的方面来说,存在将不相关的值误认为颜色的风险,因为内容相同的两个字符串被认为是相等的
const MOOD_BLUE = 'Blue';
assert.equal(COLOR_BLUE, MOOD_BLUE);我们可以通过符号解决这个问题
const COLOR_BLUE = Symbol('Blue');
const MOOD_BLUE = Symbol('Blue');
assert.notEqual(COLOR_BLUE, MOOD_BLUE);让我们使用符号值常量来实现一个函数
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);
}
}
assert.equal(getComplement(COLOR_YELLOW), COLOR_VIOLET);对象中属性(字段)的键在两个级别使用
程序在基本级别运行。该级别的键反映了问题域(程序解决问题的领域),例如
ECMAScript 和许多库在元级别运行。它们管理数据并提供不属于问题域的服务,例如
标准方法 .toString() 由 ECMAScript 在创建对象的字符串表示形式时使用(第 A 行)
const point = {
x: 7,
y: 4,
toString() {
return `(${this.x}, ${this.y})`;
},
};
assert.equal(
String(point), '(7, 4)'); // (A).x 和 .y 是基本级别的属性 - 它们用于解决使用点进行计算的问题。 .toString() 是元级别的属性 - 它与问题域无关。
标准 ECMAScript 方法 .toJSON()
const point = {
x: 7,
y: 4,
toJSON() {
return [this.x, this.y];
},
};
assert.equal(
JSON.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',
[specialMethod]() { // (A)
return this._id;
}
};
assert.equal(obj[specialMethod](), 'kf12oi');第 A 行中的方括号使我们能够指定该方法必须具有键 specialMethod。有关更多详细信息,请参阅§28.7.2 “对象字面量中的计算键”。
在 ECMAScript 中扮演特殊角色的符号称为公开已知的符号。示例包括
Symbol.iterator:使对象可迭代。它是返回迭代器的方法的键。有关此主题的更多信息,请参阅§30 “同步迭代”。
Symbol.hasInstance:自定义 instanceof 的工作方式。如果对象实现了具有该键的方法,则可以在该运算符的右侧使用它。例如
const PrimitiveNull = {
[Symbol.hasInstance](x) {
return x === null;
}
};
assert.equal(null instanceof PrimitiveNull, true);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.mjsSymbol.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();
assert.throws(
() => { obj['__'+sym+'__'] = true },
{ message: 'Cannot convert a Symbol value to a string' });缺点是异常使得使用符号更加复杂。通过加号运算符组合字符串时,必须显式转换符号
> const mySymbol = Symbol('mySymbol');
> 'Symbol I used: ' + mySymbol
TypeError: Cannot convert a Symbol value to a string
> 'Symbol I used: ' + String(mySymbol)
'Symbol I used: Symbol(mySymbol)' 测验
请参阅测验应用程序。