面向急切程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

22 符号



22.1 符号是类似于对象的基元

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

const mySymbol = Symbol('mySymbol');

参数是可选的,并提供描述,这主要用于调试。

22.1.1 符号是原始值

符号是原始值

22.1.2 符号也类似于对象

尽管符号是基元,但它们也类似于对象,因为 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

22.2 符号的描述

我们传递给符号工厂函数的参数为创建的符号提供描述

const mySymbol = Symbol('mySymbol');

可以通过两种方式访问描述。

首先,它是 .toString() 返回的字符串的一部分

assert.equal(mySymbol.toString(), 'Symbol(mySymbol)');

其次,自 ES2019 起,我们可以通过属性 .description 检索描述

assert.equal(mySymbol.description, 'mySymbol');

22.3 符号的用例

符号的主要用例是

22.3.1 符号作为常量的值

假设您要创建表示红色、橙色、黄色、绿色、蓝色和紫色的常量。一种简单的方法是使用字符串

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);

22.3.2 符号作为唯一的属性键

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

程序的基本级别和元级别必须是独立的:基本级别的属性键不应与元级别的属性键冲突。

如果我们使用名称(字符串)作为属性键,我们将面临两个挑战

以下是后者对 JavaScript 造成问题的两个示例

用作属性键的符号可以帮助我们解决这个问题:每个符号都是唯一的,并且符号键永远不会与任何其他字符串或符号键冲突。

22.3.2.1 示例:具有元级别方法的库

例如,假设我们正在编写一个库,如果对象实现了特殊方法,则该库会以不同的方式处理它们。这就是为这种方法定义属性键并在对象中实现它的样子

const specialMethod = Symbol('specialMethod');
const obj = {
  _id: 'kf12oi',
  [specialMethod]() { // (A)
    return this._id;
  }
};
assert.equal(obj[specialMethod](), 'kf12oi');

第 A 行中的方括号使我们能够指定该方法必须具有键 specialMethod。有关更多详细信息,请参阅§28.7.2 “对象字面量中的计算键”

22.4 公开已知的符号

在 ECMAScript 中扮演特殊角色的符号称为公开已知的符号。示例包括

  练习:公开已知的符号

22.5 转换符号

如果我们将符号 sym 转换为另一种原始类型,会发生什么情况?表 15 给出了答案。

表 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)'

  测验

请参阅测验应用程序