通用 Symbol

在第二章中的“Symbol”一节中,我们讲解了新的ES6基本类型symbol。除了你可以在你自己的程序中定义的symbol以外,JS预定义了几种内建symbol,被称为 通用(Well Known) Symbols(WKS)。

定义这些symbol值主要是为了向你的JS程序暴露特殊的元属性来给你更多JS行为的控制权。

我们将简要介绍每一个symbol并讨论它们的目的。

Symbol.iterator

在第二和第三章中,我们介绍并使用了@@iteratorsymbol,它被自动地用于...扩散和for..of循环。我们还在第五章中看到了在新的ES6集合中定义的@@iterator

Symbol.iterator表示在任意一个对象上的特殊位置(属性),语言机制自动地在这里寻找一个方法,这个方法将构建一个用于消费对象值的迭代器对象。许多对象都带有一个默认的Symbol.iterator

然而,我们可以通过设置Symbol.iterator属性来为任意对象定义我们自己的迭代器逻辑,即便它是覆盖默认迭代器的。这里的元编程观点是,我们在定义JS的其他部分(明确地说,是操作符和循环结构)在处理我们所定义的对象值时所使用的行为。

考虑如下代码:

  1. var arr = [4,5,6,7,8,9];
  2. for (var v of arr) {
  3. console.log( v );
  4. }
  5. // 4 5 6 7 8 9
  6. // 定义一个仅在奇数索引处产生值的迭代器
  7. arr[Symbol.iterator] = function*() {
  8. var idx = 1;
  9. do {
  10. yield this[idx];
  11. } while ((idx += 2) < this.length);
  12. };
  13. for (var v of arr) {
  14. console.log( v );
  15. }
  16. // 5 7 9

Symbol.toStringTagSymbol.hasInstance

最常见的元编程任务之一,就是在一个值上进行自省来找出它是什么 种类 的,者经常用来决定它们上面适于实施什么操作。对于对象,最常见的两个自省技术是toString()instanceof

考虑如下代码:

  1. function Foo() {}
  2. var a = new Foo();
  3. a.toString(); // [object Object]
  4. a instanceof Foo; // true

在ES6中,你可以控制这些操作的行为:

  1. function Foo(greeting) {
  2. this.greeting = greeting;
  3. }
  4. Foo.prototype[Symbol.toStringTag] = "Foo";
  5. Object.defineProperty( Foo, Symbol.hasInstance, {
  6. value: function(inst) {
  7. return inst.greeting == "hello";
  8. }
  9. } );
  10. var a = new Foo( "hello" ),
  11. b = new Foo( "world" );
  12. b[Symbol.toStringTag] = "cool";
  13. a.toString(); // [object Foo]
  14. String( b ); // [object cool]
  15. a instanceof Foo; // true
  16. b instanceof Foo; // false

在原型(或实例本身)上的@@toStringTagsymbol指定一个用于[object ___]字符串化的字符串值。

@@hasInstancesymbol是一个在构造器函数上的方法,它接收一个实例对象值并让你通过放回truefalse来决定这个值是否应当被认为是一个实例。

注意: 要在一个函数上设置@@hasInstance,你必须使用Object.defineProperty(..),因为在Function.prototype上默认的那一个是writable: false。更多信息参见本系列的 this与对象原型

Symbol.species

在第三章的“类”中,我们介绍了@@speciessymbol,它控制一个类内建的生成新实例的方法使用哪一个构造器。

最常见的例子是,在子类化Array并且想要定义slice(..)之类被继承的方法应当使用哪一个构造器时。默认地,在一个Array的子类实例上调用的slice(..)将产生这个子类的实例,坦白地说这正是你经常希望的。

但是,你可以通过覆盖一个类的默认@@species定义来进行元编程:

  1. class Cool {
  2. // 将 `@@species` 倒推至被衍生的构造器
  3. static get [Symbol.species]() { return this; }
  4. again() {
  5. return new this.constructor[Symbol.species]();
  6. }
  7. }
  8. class Fun extends Cool {}
  9. class Awesome extends Cool {
  10. // 将 `@@species` 强制为父类构造器
  11. static get [Symbol.species]() { return Cool; }
  12. }
  13. var a = new Fun(),
  14. b = new Awesome(),
  15. c = a.again(),
  16. d = b.again();
  17. c instanceof Fun; // true
  18. d instanceof Awesome; // false
  19. d instanceof Cool; // true

就像在前面的代码段中的Cool的定义展示的那样,在内建的原生构造器上的Symbol.species设定默认为return this。它在用户自己的类上没有默认值,但也像展示的那样,这种行为很容易模拟。

如果你需要定义生成新实例的方法,使用new this.constructor[Symbol.species](..)的元编程模式,而不要用手写的new this.constructor(..)或者new XYZ(..)。如此衍生的类就能够自定义Symbol.species来控制哪一个构造器来制造这些实例。

Symbol.toPrimitive

在本系列的 类型与文法 一书中,我们讨论了ToPrimitive抽象强制转换操作,它在对象为了某些操作(例如==比较或者+加法)而必须被强制转换为一个基本类型值时被使用。在ES6以前,没有办法控制这个行为。

在ES6中,在任意对象值上作为属性的@@toPrimitivesymbol都可以通过指定一个方法来自定义这个ToPrimitive强制转换。

考虑如下代码:

  1. var arr = [1,2,3,4,5];
  2. arr + 10; // 1,2,3,4,510
  3. arr[Symbol.toPrimitive] = function(hint) {
  4. if (hint == "default" || hint == "number") {
  5. // 所有数字的和
  6. return this.reduce( function(acc,curr){
  7. return acc + curr;
  8. }, 0 );
  9. }
  10. };
  11. arr + 10; // 25

Symbol.toPrimitive方法将根据调用ToPrimitive的操作期望何种类型,而被提供一个值为"string""number",或"default"(这应当被解释为"number")的 提示(hint)。在前一个代码段中,+加法操作没有提示("default"将被传递)。一个*乘法操作将提示"number",而一个String(arr)将提示"string"

警告: ==操作符将在一个对象上不使用任何提来示调用ToPrimitive操作 —— 如果存在@@toPrimitive方法的话,将使用"default"被调用 —— 如果另一个被比较的值不是一个对象。但是,如果两个被比较的值都是对象,==的行为与===是完全相同的,也就是引用本身将被直接比较。这种情况下,@@toPrimitive根本不会被调用。关于强制转换和抽象操作的更多信息,参见本系列的 类型与文法

正则表达式 Symbols

对于正则表达式对象,有四种通用 symbols 可以被覆盖,它们控制着这些正则表达式在四个相应的同名String.prototype函数中如何被使用:

  • @@match:一个正则表达式的Symbol.match值是使用被给定的正则表达式来匹配一个字符串值的全部或部分的方法。如果你为String.prototype.match(..)传递一个正则表达式做范例匹配,它就会被使用。

    匹配的默认算法写在ES6语言规范的第21.2.5.6部分(@match)。你可以覆盖这个默认算法并提供额外的正则表达式特性,比如后顾断言。"">https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@match)。你可以覆盖这个默认算法并提供额外的正则表达式特性,比如后顾断言。

    Symbol.match还被用于isRegExp抽象操作(参见第六章的“字符串检测函数”中的注意部分)来判定一个对象是否意在被用作正则表达式。为了使一个这样的对象不被看作是正则表达式,可以将Symbol.match的值设置为false(或falsy的东西)强制这个检查失败。

  • @@replace:一个正则表达式的Symbol.replace值是被String.prototype.replace(..)使用的方法,来替换一个字符串里面出现的一个或所有字符序列,这些字符序列匹配给出的正则表达式范例。

    替换的默认算法写在ES6语言规范的第21.2.5.8部分(@replace)。"">https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@replace)。

    一个覆盖默认算法的很酷的用法是提供额外的replacer可选参数值,比如通过用连续的替换值消费可迭代对象来支持"abaca".replace(/a/g,[1,2,3])产生"1b2c3"

  • @@search:一个正则表达式的Symbol.search值是被String.prototype.search(..)使用的方法,来在一个字符串中检索一个匹配给定正则表达式的子字符串。

    检索的默认算法写在ES6语言规范的第21.2.5.9部分(@search)。"">https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@search)。

  • @@split:一个正则表达式的Symbol.split值是被String.prototype.split(..)使用的方法,来将一个字符串在分隔符匹配给定正则表达式的位置分割为子字符串。

    分割的默认算法写在ES6语言规范的第21.2.5.11部分(@split)。"">https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@split)。

覆盖内建的正则表达式算法不是为心脏脆弱的人准备的!JS带有高度优化的正则表达式引擎,所以你自己的用户代码将很可能慢得多。这种类型的元编程很精巧和强大,但是应当仅用于确实必要或有好处的情况下。

Symbol.isConcatSpreadable

@@isConcatSpreadablesymbol可以作为一个布尔属性(Symbol.isConcatSpreadable)在任意对象上(比如一个数组或其他的可迭代对象)定义,来指示当它被传递给一个数组concat(..)时是否应当被 扩散

考虑如下代码:

  1. var a = [1,2,3],
  2. b = [4,5,6];
  3. b[Symbol.isConcatSpreadable] = false;
  4. [].concat( a, b ); // [1,2,3,[4,5,6]]

Symbol.unscopables

@@unscopablessymbol可以作为一个对象属性(Symbol.unscopables)在任意对象上定义,来指示在一个with语句中哪一个属性可以和不可以作为此法变量被暴露。

考虑如下代码:

  1. var o = { a:1, b:2, c:3 },
  2. a = 10, b = 20, c = 30;
  3. o[Symbol.unscopables] = {
  4. a: false,
  5. b: true,
  6. c: false
  7. };
  8. with (o) {
  9. console.log( a, b, c ); // 1 20 3
  10. }

一个在@@unscopables对象中的true指示这个属性应当是 非作用域(unscopable) 的,因此会从此法作用域变量中被过滤掉。false意味着它可以被包含在此法作用域变量中。

警告: with语句在strict模式下是完全禁用的,而且因此应当被认为是在语言中被废弃的。不要使用它。更多信息参见本系列的 作用域与闭包。因为应当避免with,所以这个@@unscopablessymbol也是无意义的。