通用 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的其他部分(明确地说,是操作符和循环结构)在处理我们所定义的对象值时所使用的行为。

考虑如下代码:

var arr = [4,5,6,7,8,9];

for (var v of arr) {
    console.log( v );
}
// 4 5 6 7 8 9

// 定义一个仅在奇数索引处产生值的迭代器
arr[Symbol.iterator] = function*() {
    var idx = 1;
    do {
        yield this[idx];
    } while ((idx += 2) < this.length);
};

for (var v of arr) {
    console.log( v );
}
// 5 7 9

Symbol.toStringTagSymbol.hasInstance

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

考虑如下代码:

function Foo() {}

var a = new Foo();

a.toString();                // [object Object]
a instanceof Foo;            // true

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

function Foo(greeting) {
    this.greeting = greeting;
}

Foo.prototype[Symbol.toStringTag] = "Foo";

Object.defineProperty( Foo, Symbol.hasInstance, {
    value: function(inst) {
        return inst.greeting == "hello";
    }
} );

var a = new Foo( "hello" ),
    b = new Foo( "world" );

b[Symbol.toStringTag] = "cool";

a.toString();                // [object Foo]
String( b );                // [object cool]

a instanceof Foo;            // true
b instanceof Foo;            // false

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

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

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

Symbol.species

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

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

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

class Cool {
    // 将 `@@species` 倒推至被衍生的构造器
    static get [Symbol.species]() { return this; }

    again() {
        return new this.constructor[Symbol.species]();
    }
}

class Fun extends Cool {}

class Awesome extends Cool {
    // 将 `@@species` 强制为父类构造器
    static get [Symbol.species]() { return Cool; }
}

var a = new Fun(),
    b = new Awesome(),
    c = a.again(),
    d = b.again();

c instanceof Fun;            // true
d instanceof Awesome;        // false
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强制转换。

考虑如下代码:

var arr = [1,2,3,4,5];

arr + 10;                // 1,2,3,4,510

arr[Symbol.toPrimitive] = function(hint) {
    if (hint == "default" || hint == "number") {
        // 所有数字的和
        return this.reduce( function(acc,curr){
            return acc + curr;
        }, 0 );
    }
};

arr + 10;                // 25

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

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

正则表达式 Symbols

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

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

Symbol.isConcatSpreadable

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

考虑如下代码:

var a = [1,2,3],
    b = [4,5,6];

b[Symbol.isConcatSpreadable] = false;

[].concat( a, b );        // [1,2,3,[4,5,6]]

Symbol.unscopables

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

考虑如下代码:

var o = { a:1, b:2, c:3 },
    a = 10, b = 20, c = 30;

o[Symbol.unscopables] = {
    a: false,
    b: true,
    c: false
};

with (o) {
    console.log( a, b, c );        // 1 20 3
}

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

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

results matching ""

    No results matching ""