18.符号

符号是通过工厂函数Symbol()创建的原始值:

const mySymbol = Symbol('mySymbol');

该参数是可选的,并提供了一个描述,主要用于调试。

一方面,符号就像对象一样,Symbol()创建的每个值都是唯一的,不按值进行比较:

> Symbol() === Symbol()
false

另一方面,它们也表现得像原始值 - 它们必须通过typeof进行分类,它们可以是对象中的属性键:

const sym = Symbol();
assert.equal(typeof sym, 'symbol');
const obj = {
  [sym]: 123,
};

20.1 符号用例

符号的主要用例是:

  • 用于值的常量
  • 唯一的属性键

20.1.1 符号:用于值的常量

假设您要创建表示红色,橙色,黄色,绿色,蓝色和紫色的常量。这样做的一个简单方法是使用字符串:

const COLOR_BLUE = 'Blue';

一方面,记录该常量会产生有用的输出。另一方面,存在将无关值误认为颜色的风险,因为具有相同内容的两个字符串被认为是相等的:

const MOOD_BLUE = 'Blue';
assert.equal(COLOR_BLUE, MOOD_BLUE);

我们可以通过符号来解决这个问题:

const COLOR_BLUE = Symbol('Blue');
const MOOD_BLUE = '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);

值得注意的是,如果用'Blue'调用它,该函数会抛出异常:

assert.throws(() => getComplement('Blue'));

20.1.2 符号:唯一的属性键

对象中属性(字段)的键在两个级别使用:

  • 程序在基础级别上运行。这一级别上的键反映了程序所解决的问题。
  • 库和 ECMAScript 在元级别上运行。这一级别的键由服务使用,他们运行在基础级别的数据和代码上。 一个这样的键是toString

以下代码演示了不同之处:

const point = {
  x: 7,
  y: 4,
  toString() {
    return `(${this.x}, ${this.y})`;
  },
};
assert.equal(String(point), '(7, 4)');

属性.x.y存在于基础级别。它们是由point编码的点的坐标,反映了问题领域。方法.toString()是元级别属性。它告诉 JavaScript 如何对此对象进行字符串化。

元级别和基础级别绝不能发生冲突,这在编程语言生命后期引入新机制时变得更加困难。

符号可用作属性键并解决此问题:每个符号都是唯一的,不会与任何字符串或任何其他符号冲突。

作为一个例子,我们假设我们正在编写一个库,如果它们实现了一个特殊的方法,它会以不同的方式对待它们。这就是定义这种方法的属性键并为对象实现它的方法如下所示:

const specialMethod = Symbol('specialMethod');
const obj = {
  [specialMethod](x) {
    return x + x;
  }
};
assert.equal(obj[specialMethod]('abc'), 'abcabc');

对象 的章节中更详细地解释了这种语法。

20.2 众所周知的符号

在 ECMAScript 中起到特殊作用的符号称为公认符号 。例子包括:

  • Symbol.iterator:使对象可迭代 。它是返回迭代器的方法键。
  • Symbol.hasInstance:自定义instanceof的工作方式。如果对象使用该键实现方法,则可以在该运算符的右侧使用它。例如:
    class PrimitiveNull {
      static [Symbol.hasInstance](x) {
        return x === null;
      }
    }
    assert.equal(null instanceof PrimitiveNull, true);
    Symbol.toStringTag: influences the default .toString() method.
    
  • Symbol.toStringTag:影响默认.toString()方法。 注意:覆盖.toString()通常更好。
    > String({})
    '[object Object]'
    > String({ [Symbol.toStringTag]: 'is no money' })
    '[object is no money]'
    

练习:公认符号

  • Symbol.toStringTagexercises/symbols/to_string_tag_test.js
  • Symbol.hasInstanceexercises/symbols/has_instance_test.js

20.3 转换符号

如果我们将符号sym转换为另一种原始类型会发生什么?表 17 就是答案。

Table 17: 将符号转换为其他原始类型的结果。

转换为 显式转换 强制转换(隐式转换)
布尔值 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)'
下一节:本章介绍以下控制流语句:
if语句(ES1)
switch语句(ES3)
while循环(ES1)
do-while循环(ES3)
for循环(ES1)
for-of循环(ES6)
for-await-of循环(ES2018)
for-in循环(ES1)