Symbol

在ES6中,长久以来首次,有一个新的基本类型被加入到了JavaScript:symbol。但是,与其他的基本类型不同,symbol没有字面形式。

这是你如何创建一个symbol:

  1. var sym = Symbol( "some optional description" );
  2. typeof sym; // "symbol"

一些要注意的事情是:

  • 你不能也不应该将newSymbol(..)一起使用。它不是一个构造器,你也不是在产生一个对象。
  • 被传入Symbol(..)的参数是可选的。如果传入的话,它应当是一个字符串,为symbol的目的给出一个友好的描述。
  • typeof的输出是一个新的值("symbol"),这是识别一个symbol的主要方法。

如果描述被提供的话,它仅仅用于symbol的字符串化表示:

  1. sym.toString(); // "Symbol(some optional description)"

与基本字符串值如何不是String的实例的原理很相似,symbol也不是Symbol的实例。如果,由于某些原因,你想要为一个symbol值构建一个封箱的包装器对像,你可以做如下的事情:

  1. sym instanceof Symbol; // false
  2. var symObj = Object( sym );
  3. symObj instanceof Symbol; // true
  4. symObj.valueOf() === sym; // true

注意: 在这个代码段中的symObjsym是可以互换使用的;两种形式可以在symbol被用到的地方使用。没有太多的理由要使用封箱的包装对象形式(symObj),而不用基本类型形式(sym)。和其他基本类型的建议相似,使用sym而非symObj可能是最好的。

一个symbol本身的内部值 —— 称为它的name —— 被隐藏在代码之外而不能取得。你可以认为这个symbol的值是一个自动生成的,(在你的应用程序中)独一无二的字符串值。

但如果这个值是隐藏且不可取得的,那么拥有一个symbol还有什么意义?

一个symbol的主要意义是创建一个不会和其他任何值冲突的类字符串值。所以,举例来说,可以考虑将一个symbol用做表示一个事件的名称的值:

  1. const EVT_LOGIN = Symbol( "event.login" );

然后你可以在一个使用像"event.login"这样的一般字符串字面量的地方使用EVT_LOGIN

  1. evthub.listen( EVT_LOGIN, function(data){
  2. // ..
  3. } );

其中的好处是,EVT_LOGIN持有一个不能被其他任何值所(有意或无意地)重复的值,所以在哪个事件被分发或处理的问题上不可能存在任何含糊。

注意: 在前面的代码段的幕后,几乎可以肯定地认为evthub工具使用了EVT_LOGIN参数值的symbol值作为某个跟踪事件处理器的内部对象的属性/键。如果evthub需要将symbol值作为一个真实的字符串使用,那么它将需要使用String(..)或者toString(..)进行明确强制转换,因为symbol的隐含字符串强制转换是不允许的。

你可能会将一个symbol直接用做一个对象中的属性名/键,如此作为一个你想将之用于隐藏或元属性的特殊属性。重要的是,要知道虽然你试图这样对待它,但是它 实际上 并不是隐藏或不可接触的属性。

考虑这个实现了 单例 模式行为的模块 —— 也就是,它仅允许自己被创建一次:

  1. const INSTANCE = Symbol( "instance" );
  2. function HappyFace() {
  3. if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];
  4. function smile() { .. }
  5. return HappyFace[INSTANCE] = {
  6. smile: smile
  7. };
  8. }
  9. var me = HappyFace(),
  10. you = HappyFace();
  11. me === you; // true

这里的symbol值INSTANCE是一个被静态地存储在HappyFace()函数对象上的特殊的,几乎是隐藏的,类元属性。

替代性地,它本可以是一个像__instance这样的普通属性,而且其行为将会是一模一样的。symbol的使用仅仅增强了程序元编程的风格,将这个INSTANCE属性与其他普通的属性间保持隔离。

Symbol注册表

在前面几个例子中使用symbol的一个微小的缺点是,变量EVT_LOGININSTANCE不得不存储在外部作用域中(甚至也许是全局作用域),或者用某种方法存储在一个可用的公共位置,这样代码所有需要使用这些symbol的部分都可以访问它们。

为了辅助组织访问这些symbol的代码,你可以使用 全局symbol注册表 来创建symbol。例如:

  1. const EVT_LOGIN = Symbol.for( "event.login" );
  2. console.log( EVT_LOGIN ); // Symbol(event.login)

和:

  1. function HappyFace() {
  2. const INSTANCE = Symbol.for( "instance" );
  3. if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];
  4. // ..
  5. return HappyFace[INSTANCE] = { .. };
  6. }

Symbol.for(..)查询全局symbol注册表来查看一个symbol是否已经使用被提供的说明文本存储过了,如果有就返回它。如果没有,就创建一个并返回。换句话说,全局symbol注册表通过描述文本将symbol值看作它们本身的单例。

但这也意味着只要使用匹配的描述名,你的应用程序的任何部分都可以使用Symbol.for(..)从注册表中取得symbol。

讽刺的是,基本上symbol的本意是在你的应用程序中取代 魔法字符串 的使用(被赋予了特殊意义的随意的字符串值)。但是你正是在全局symbol注册表中使用 魔法 描述字符串值来唯一识别/定位它们的!

为了避免意外的冲突,你可能想使你的symbol描述十分独特。这么做的一个简单的方法是在它们之中包含前缀/环境/名称空间的信息。

例如,考虑一个像下面这样的工具:

  1. function extractValues(str) {
  2. var key = Symbol.for( "extractValues.parse" ),
  3. re = extractValues[key] ||
  4. /[^=&]+?=([^&]+?)(?=&|$)/g,
  5. values = [], match;
  6. while (match = re.exec( str )) {
  7. values.push( match[1] );
  8. }
  9. return values;
  10. }

我们使用魔法字符串值"extractValues.parse",因为在注册表中的其他任何symbol都不太可能与这个描述相冲突。

如果这个工具的一个用户想要覆盖这个解析用的正则表达式,他们也可以使用symbol注册表:

  1. extractValues[Symbol.for( "extractValues.parse" )] =
  2. /..some pattern../g;
  3. extractValues( "..some string.." );

除了symbol注册表在全局地存储这些值上提供的协助以外,我们在这里看到的一切其实都可以通过将魔法字符串"extractValues.parse"作为一个键,而不是一个symbol,来做到。这其中在元编程的层次上的改进要多于在函数层次上的改进。

你可能偶然会使用一个已经被存储在注册表中的symbol值来查询它底层存储了什么描述文本(键)。例如,因为你无法传递symbol值本身,你可能需要通知你的应用程序的另一个部分如何在注册表中定位一个symbol。

你可以使用Symbol.keyFor(..)取得一个被注册的symbol描述文本(键):

  1. var s = Symbol.for( "something cool" );
  2. var desc = Symbol.keyFor( s );
  3. console.log( desc ); // "something cool"
  4. // 再次从注册表取得symbol
  5. var s2 = Symbol.for( desc );
  6. s2 === s; // true

Symbols作为对象属性

如果一个symbol被用作一个对象的属性/键,它会被以一种特殊的方式存储,以至这个属性不会出现在这个对象属性的普通枚举中:

  1. var o = {
  2. foo: 42,
  3. [ Symbol( "bar" ) ]: "hello world",
  4. baz: true
  5. };
  6. Object.getOwnPropertyNames( o ); // [ "foo","baz" ]

要取得对象的symbol属性:

  1. Object.getOwnPropertySymbols( o ); // [ Symbol(bar) ]

这表明一个属性symbol实际上不是隐藏的或不可访问的,因为你总是可以在Object.getOwnPropertySymbols(..)的列表中看到它。

内建Symbols

ES6带来了好几种预定义的内建symbol,它们暴露了在JavaScript对象值上的各种元行为。然而,正如人们所预料的那样,这些symbol 没有 没被注册到全局symbol注册表中。

取而代之的是,它们作为属性被存储到了Symbol函数对象中。例如,在本章早先的“for..of”一节中,我们介绍了值Symbol.iterator

  1. var a = [1,2,3];
  2. a[Symbol.iterator]; // native function

语言规范使用@@前缀注释指代内建的symbol,最常见的几个是:@@iterator@@toStringTag@@toPrimitive。还定义了几个其他的symbol,虽然他们可能不那么频繁地被使用。

注意: 关于这些内建symbol如何被用于元编程的详细信息,参见第七章的“通用Symbol”。