通用 Symbol
在第二章中的“Symbol”一节中,我们讲解了新的ES6基本类型symbol
。除了你可以在你自己的程序中定义的symbol以外,JS预定义了几种内建symbol,被称为 通用(Well Known) Symbols(WKS)。
定义这些symbol值主要是为了向你的JS程序暴露特殊的元属性来给你更多JS行为的控制权。
我们将简要介绍每一个symbol并讨论它们的目的。
Symbol.iterator
在第二和第三章中,我们介绍并使用了@@iterator
symbol,它被自动地用于...
扩散和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.toStringTag
和 Symbol.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
在原型(或实例本身)上的@@toStringTag
symbol指定一个用于[object ___]
字符串化的字符串值。
@@hasInstance
symbol是一个在构造器函数上的方法,它接收一个实例对象值并让你通过放回true
或false
来决定这个值是否应当被认为是一个实例。
注意: 要在一个函数上设置@@hasInstance
,你必须使用Object.defineProperty(..)
,因为在Function.prototype
上默认的那一个是writable: false
。更多信息参见本系列的 this与对象原型。
Symbol.species
在第三章的“类”中,我们介绍了@@species
symbol,它控制一个类内建的生成新实例的方法使用哪一个构造器。
最常见的例子是,在子类化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中,在任意对象值上作为属性的@@toPrimitive
symbol都可以通过指定一个方法来自定义这个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
函数中如何被使用:
@@match
:一个正则表达式的Symbol.match
值是使用被给定的正则表达式来匹配一个字符串值的全部或部分的方法。如果你为String.prototype.match(..)
传递一个正则表达式做范例匹配,它就会被使用。匹配的默认算法写在ES6语言规范的第21.2.5.6部分(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部分(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部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@search)。
@@split
:一个正则表达式的Symbol.split
值是被String.prototype.split(..)
使用的方法,来将一个字符串在分隔符匹配给定正则表达式的位置分割为子字符串。分割的默认算法写在ES6语言规范的第21.2.5.11部分(https://people.mozilla.org/~jorendorff/es6-draft.html#sec-regexp.prototype-@@split)。
覆盖内建的正则表达式算法不是为心脏脆弱的人准备的!JS带有高度优化的正则表达式引擎,所以你自己的用户代码将很可能慢得多。这种类型的元编程很精巧和强大,但是应当仅用于确实必要或有好处的情况下。
Symbol.isConcatSpreadable
@@isConcatSpreadable
symbol可以作为一个布尔属性(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
@@unscopables
symbol可以作为一个对象属性(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
,所以这个@@unscopables
symbol也是无意义的。