本章我们将介绍 JavaScript symbol,以及依赖于此的 JavaScript 新特性。
什么是 JavaScript 中的基本数据类型 primitive data types?简单来说,就是null、undefined、string、number和boolean这几种数据类型。还有别的吗?是的!符号(symbol)是在 ES2016(ES6)中与bigint一起引入的一种新的基本数据类型。
typeof null; // 'object' (it's a bug) typeof undefined; // 'undefined' typeof 100; // 'number' typeof "hello"; // 'string' typeof true; // 'boolean' typeof Symbol(); // 'symbol' typeof 100n; // 'bigint'
本章我们主要讨论符号。符号很有趣,与之前你见到的那些数据类型都不同。符号是一种基本数据类型,但是你不能写出它的字面量形式,因为它没有字面量。
var sym = Symbol(description);
为了创建符号,需要像上面那样调用Symbol函数。该函数返回唯一的符号值。符号的内部实现取决于实现者(也就是 JavaScript 引擎),但是,符号不能是string、number或boolean值。来看下面的代码:
// create some symbols
var sym1 = Symbol();
var sym2 = Symbol();
var sym3 = Symbol('apple');
var sym4 = Symbol('apple');
// type check
console.log( '"typeof sym1" =>', typeof sym1 ); // "typeof sym1" => symbol
console.log( '"typeof sym3" =>', typeof sym3 ); // "typeof sym3" => symbol
// equality check
console.log( '"sym1 === sym2" =>', sym1 === sym2 ); // "sym1 === sym2" => false
console.log( '"sym1 == sym2" =>', sym1 == sym2 ); // "sym1 == sym2" => false
console.log( '"sym3 === sym4" =>', sym3 === sym4 ); // "sym3 === sym4" => false
console.log( '"sym1 === sym2" =>', sym1 === sym2 ); // "sym1 === sym2" => false
console.log( '"sym1 === sym1" =>', sym1 === sym1 ); // "sym1 === sym1" => true
// logging a symbol
console.log( '"sym1" =>', sym1 ); // "sym1" => Symbol()
console.log( '"sym3" =>', sym3 ); // "sym3" => Symbol(apple) Symbol函数始终返回新的符号。它接受一个description参数,用于开发调试时将该符号记录到控制台。这个参数并不会影响到符号的实际值。
由Symbol()函数调用创建的两个符号永远不能相等。因为每一次Symbol()的调用,不管是否有description参数,始终返回具有唯一值的新的符号。因此,符号只能与其自身相等。
由于符号的值都是唯一的,并且在运行时不可见,也就不能以常见的字符串或数值这样的字面量显示。因此,并没有类似var sym = #$@%#*#;这样的语法来创建一个符号:你只能通过调用Symbol()函数。
值得注意的是,Symbol函数与其它函数有所不同,它不是可构造的,也就是说,不能使用类似new Symbol()这样的表达式,否则会抛出TypeError: Symbol is not a constructor这样的异常。然而,这个函数却提供了一些静态方法,我们会在后面的部分详细介绍。
符号可以当做对象的键。由于符号不是字符串,没有字面量形式,我们需要使用方括号语法,如同使用string变量一样。
// create a new symbol
var sym = Symbol('salary');
// symbol as a property of an object
var person = {
name: 'Ross',
[ sym ]: 200,
[ Symbol('age') ]: 21,
};
// access normal property
console.log( 'person.name =>', person.name ); // person.name => Ross
console.log( 'person["name"] =>', person[ "name" ] ); // person["name"] => Ross
// access symbol property
console.log( 'person[sym] =>', person[ sym ] ); // person[sym] => 200
// assign new value
person[ sym ] = 300;
person[ Symbol('age') ] = 22; // oops
// delete property
delete person[ sym ];
// inspect object
console.log( 'person =>', person ); // person => { name: 'Ross', [Symbol(age)]: 21, [Symbol(age)]: 22 }
// not gonna happen dude
console.log( 'person[ "Symbol(age)" ] =>', person[ "Symbol(age)" ]); // person[ "Symbol(age)" ] => undefined 上面的例子中,我们创建了sym符号表示person对象的收入。针对symbol类型的属性名进行读取、写入以及删除的唯一方法,就是将原始的符号值保存到变量中,然后使用[]记号。
如果你丢失了代表对象属性的符号值,那就很难再访问到对象这个属性了。正如上面显示的那样,使用[Symbol()]语法并不是一个好主意,因为你再也无法访问到这个符号对应的属性的值了。
Symbol类型的对象属性是不可枚举的,这意味着它们不会出现在Object.keys()的返回值中(也就是拥有/可枚举属性),也不能使用for-in循环访问到(也就是自己拥有+继承/可枚举属性),同样也不在Object.getOwnPropertyNames()方法的返回值中(即拥有/可枚举+不可枚举属性)。
// create a new symbol
var sym = Symbol('salary');
// symbol as a property of an object
var person = {
name: 'Ross',
[ sym ]: 200,
[ Symbol('age') ]: 21,
};
// check enumerable properties using `Object.keys`
console.log( 'Object.keys() =>', Object.keys( person ) ); // Object.keys() => [ 'name' ]
// check properties using `Object.getOwnPropertyNames`
console.log( 'Object.getOwnPropertyNames() =>', Object.getOwnPropertyNames( person ) ); // Object.getOwnPropertyNames() => [ 'name' ]
// check properties using `for-in` loop
for( let prop in person ) {
console.log( 'for-in prop =>', prop ); // for-in prop => name
} 我不会将这一特性看作是一个缺点,而是当成是一种强大的抽象特性。因为符号被当作不可枚举属性,就可以保存程序中的特殊含义的数据。一种必须使用原始符号的引用才能写入的符号属性。这对于导出对象的模块很有用,因为你可以只导出包含符号属性的对象,而不导出符号,这样,该属性值就永远不能被覆盖。
虽然符号属性是不可枚举的,但它的属性描述符的enumerable属性却被设置为true。初看起来这很奇怪,不过却有着必要的原因。阅读这个问答可以了解更多。
不过,JavaScript 提供了Object.getOwnPropertySymbols()函数,返回对象所拥有的属性类型为符号的列表。所以,不能把符号属性当做是私有属性。符号只是很难访问,但并不是不能访问。
// create a new symbol
var sym = Symbol('salary');
// symbol as a property of an object
var person = {
name: 'Ross',
[ sym ]: 200,
[ Symbol('age') ]: 21,
};
// get symbol properties using `getOwnPropertySymbols`
const symbols = Object.getOwnPropertySymbols( person );
console.log( 'symbols =>', symbols ); // symbols => [ Symbol(salary), Symbol(age) ]
// find symbol that represents `age`
const ageSym = symbols.find( sym => {
return sym.description &&
sym.description.includes( 'age' );
} );
console.log( 'ageSym =>', ageSym ); // ageSym => Symbol(age)
console.log( 'person[ageSym] =>', person[ageSym] ); // person[ageSym] => 21 还有一件事,每一个符号都有一个description属性,返回在Symbol(description)调用时传入的description的值。如果在创建时没有传入,则返回undefined。在上面的例子中,我们利用这一技巧去获取描述为age的符号所对应的属性值,虽然这种写法并不聪明。
现在,至少有一件事是肯定的:符号并不是string。然而,这一点在某些期望由符号返回string的情景下,可能会造成一定的问题。例如,如果我们在字符串插值语句中加入一个符号,或者将带有符号的对象序列化为 JSON,会发生什么?
// create a new symbol
var sym = Symbol('salary');
// symbol as a property of an object
var person = {
name: 'Ross',
[ sym ]: 200,
[ Symbol('age') ]: 21,
};
// convert `person` to JSON string
console.log( 'result/json ->', JSON.stringify( person ) ); // result/json -> {"name":"Ross"}
// interpolate symbol as string
try {
console.log( `salary key is -> ${ sym }` );
} catch( err ) {
console.log( 'result/error ->', err ); // result/error -> TypeError: Cannot convert a Symbol value to a string
}
// perform string concatenation
try {
console.log( 'salary key is ->' + sym );
} catch( err ) {
console.log( 'result/error ->', err ); // result/error -> TypeError: Cannot convert a Symbol value to a string
} 上面的代码中,JSON.stringify简单地将属性名类型为符号的属性全部忽略,然后将其余字段序列化,没有任何错误。然而,那些试图将符号当做字符串读取的操作,都会抛出TypeError。
你可能想知道,为什么之前使用console.log可以将符号值显示为string,这是因为console.log调用了符号的toString函数。你也可以手动调用sym.toString()函数,该函数返回Symbol(salary)字符串。
全局符号注册表
我没有对你说实话。其实还是有一种方法,让你在没有符号的引用的时候,可以访问到这个符号的,甚至是在每次创建的时候并不一定返回唯一的符号。Symbol.for(key)函数从全局注册表返回具有唯一的key的符号;如果没有这样的符号,则直接创建然后返回。
全局注册表对于每一个脚本文件、每一个模块、每一个上下文(比如 iframe、web worker、service worker 等),都是相同的。因此,这个注册表被称为运行时范围的符号注册表。所以,当你通过Symbol.for(key)访问时,每次都可以得到同一个符号。
Symbol.keyFor(sym)返回一个符号的key值,注意,只有当这个符号是运行时范围的,才会返回,否则返回undefined。
// create a new symbol
var sym = Symbol.for('salary');
console.log( "sym's description ->", sym.description ); // sym's description -> salary
console.log( "sym's key ->", Symbol.keyFor(sym) ); // sym's key -> salary
// check for equality
console.log( "Symbol.for('salary') === sym ->", Symbol.for('salary') === sym ); // Symbol.for('salary') === sym -> true
// symbol as a property of an object
var person = {
name: 'Ross',
[ sym ]: 200,
[ Symbol.for('age') ]: 21,
};
// access symbol property
console.log( 'person[sym] ->', person[ sym ] ); // person[sym] -> 200
console.log( 'person[Symbol.for("salary")] ->', person[ Symbol.for('salary') ] ); // person[Symbol.for("salary")] -> 200
// inspect object
console.log( 'person ->', person ); // person -> { name: 'Ross', [Symbol(salary)]: 200, [Symbol(age)]: 21 }
// working just fine
console.log( 'person[Symbol.for("age")] ->', person[ Symbol.for('age') ] ); // person[Symbol.for("age")] -> 21 当我们使用Symbol.for(key)表达式创建符号时,key不仅是符号的唯一标识符,而且会成为其description属性值,这是合理的,因为这种方式创建符合是没有办法设置描述的。
如果你在寻找如何从全局注册表中移除符号,你最好放弃,因为并不存在这样的函数。
公开的符号
创建符号,并且将其在整个应用内共享的最好的方法是什么?或许你应该使用Symbol.for(key)实现,因为这样创建的符号可以在整个上下文中访问到。JavaScript 提供了一些预定义的符号(运行时范围内的),用于某些特定的情景。
如果对象的属性(自己拥有的或者是继承的)具有特殊的含义,那么,使用string类型的属性名并不是一个好主意,因为很容易将其重写,我们在前面的文章中见识过valueOf函数的例子。
JavaScript 的对象有一大堆继承而来的特殊属性,例如valueOf和toString,其目的就是为了要重写(并不是无意的重写),以便实现对象的自定义行为。我们可以看看下面的例子。
// define a simple JavaScript object
var person = {
name: 'Ross',
salary: 200,
age: 21,
};
// arithmetic operation triggers `valueOf` method
console.log( 'before: person - 10 ->', person - 10 ); // before: person - 10 -> NaN
// string operation triggers `toString` method
console.log( 'before: `Hello ${ person }` ->', `Hello ${ person }` ); // before: `Hello ${ person }` -> Hello [object Object]
/*---------*/
// provide `valueOf` and `toString` implementation
person.valueOf = function() {
return this.salary;
}
person.toString = function() {
return this.name;
}
console.log( 'after: person - 10 ->', person - 10 ); // after: person - 10 -> 190
console.log( 'after: `Hello ${ person }` ->', `Hello ${ person }` ); // after: `Hello ${ person }` -> Hello Ross 每一个继承Object的对象都有valueOf和toString的默认实现,这两个函数是在Object定义的。当针对对象执行算术运算时,会自动调用valueOf函数;当需要将对象转换为字符串时,则调用toString函数。
toString默认实现是返回[object Object]字符串,这就是一个对象的字符串表示。valueOf的默认实现是返回对象本身,因此,任何算术操作都会返回NaN,因为最终结果不是一个数字。
通过在对象本身或者原型链上增加同名的函数值,我们可以覆盖这些默认实现。因此,如果你有一个类的话,你可以提供toString和valueOf实例方法,覆盖默认行为。看下面的代码。
class Person {
constructor( name, salary, age ) {
this.name = name;
this.salary = salary;
this.age = age;
}
valueOf() {
return this.salary;
}
toString() {
return this.name;
}
}
// create an instance (object) of `Person`
var person = new Person( 'Ross', 200, 21 );
// perform operations on person
console.log( 'person - 10 ->', person - 10 ); // person - 10 -> 190
console.log( '`Hello ${ person }` ->', `Hello ${ person }` ); // `Hello ${ person }` -> Hello Ross Symbol.toPrimitive
上面这些特殊方法(属性)有一个问题是,它们很容易被错误地覆盖。例如toString和valueOf,对象(字符串或数字)的原始值在某些上下文中由这两个方法被分为两种不同类型,有时很容易引起误会。
为了解决这些问题,JavaScript 提供了一系列公开的符号,用于这些特殊属性的名字。其中之一就是toPrimitive符号。所有这些公开的符号都是Symbol对象(实际是一个函数)的公共的静态属性,并且在整个程序范围内共享。
Symbol.toOrimitive符号的作用一次性实现了toString和valueOf的作用,并且具有更高的优先级。如果对象有一个名为Symbol.toPrimitive的方法(不论是自己所有还是继承而来),在期望一个对象的原始值的时候,这个函数就会被调用,其参数hint即指明需要何种类型的原始值。
// define a simple JavaScript object
var person = {
name: 'Ross',
salary: 200,
age: 21,
};
// define `Symbol.toPrimitive` method
person[ Symbol.toPrimitive ] = function( hint ) {
if( hint === 'number' ) {
return this.salary;
} else if( hint === 'string' ) {
return this.name;
} else {
return 'person-default--';
}
}
// demands a `number` primitive value
console.log( 'person - 10 ->', person - 10 ); // person - 10 -> 190
// demands a `string` primitive value
console.log( `Hello ${person} ->`, `Hello ${person}` ); // Hello ${person} -> Hello Ross
// demands a `default` primitive value
console.log( `person + true ->`, person + true ); // person + true -> person-default--true
console.log( `person + 1 ->`, person + 1 ); // person + 1 -> person-default--1
console.log( `person + '' ->`, person + '' ); // person + '' -> person-default-- 该函数的参数hint的值可能为"number"、"string"或"default",这取决于该对象要进行哪种操作。在使用+运算符时,hint的值为"default",因为这可以是算术加运算(此时是"number"),也可以是字符串的拼接操作(此时则是"string")。
Symbol.toStringTag
我们知道,将一个对象与一个string值相加,会得到一个奇怪的字符串:[object Object]。这是因为Object的toString函数的默认实现就是返回这样的字符串。
不过,这并非只有对象(也就是Object的子类)才有的情况。JavaScript 对大多数类都提供了类似的实现。
// get string representation of a `value` using `toString` prototype method of the `Object`
function stringRepr( value ) {
return Object.prototype.toString.call( value );
}
console.log( 'undefined ->', stringRepr( undefined ) ); // undefined -> [object Undefined]
console.log( 'null ->', stringRepr( null ) ); // null -> [object Null]
console.log( 'string ->', stringRepr( '' ) ); // string -> [object String]
console.log( 'number ->', stringRepr( 1 ) ); // number -> [object Number]
console.log( 'boolean ->', stringRepr( true ) ); // boolean -> [object Boolean]
console.log( 'symbol ->', stringRepr( Symbol() ) ); // symbol -> [object Symbol]
console.log( '\nobject ->', stringRepr( {} ) ); // object -> [object Object]
console.log( 'function ->', stringRepr( function(){} ) ); // function -> [object Function]
console.log( 'array ->', stringRepr( [] ) ); // array -> [object Array]
console.log( 'Math ->', stringRepr( Math ) ); // Math -> [object Math]
console.log( 'JSON ->', stringRepr( JSON ) ); // JSON -> [object JSON] 如果你对Undefined和Null感到奇怪,大可不必。这些构造器函数并不能在运行时访问,它们纯粹是在 JavaScript 引擎内部实现的。
JavaScript 中,每一个单独的值都是由构造器创建而来,这就是为什么我们会说,JavaScript 中的所有值都是对象。每一种类都提供了各自实现的返回字符串的函数,用于描述该对象。如果没有提供自己的实现,那么就会使用Object的实现。
正如上面的运行结果所示,字符串"[object <Tag>]"中,只有Tag部分是不同的。JavaScript 提供了修改Tag值的强大功能,这可以让我们理解起来更简单一些。
// define a simple JavaScript object
var person = {
name: 'Ross',
salary: 200,
age: 21,
};
// before: the `toString` call
console.log( `before: ${ person }` ); // before: [object Object]
/*-------*/
// provide `Symbol.toStringTag` implementation
person[ Symbol.toStringTag ] = 'Person';
// after: the `toString` call
console.log( `after: ${ person }` ); // after: [object Person] 对象的Symbol.toStringTag属性,在toString函数隐式或显式调用时,会作为该对象的Tag。我们可以针对对象提供该属性,或者在对象原型链上提供。如果你使用的是类,那么,可以把它作为 getter 使用。
Symbol.hasInstance
想象一下,现在有两个类,这两个类有一些相同的字段。通常,你可以选择使用继承,让一个类继承另外的类,从而获得共有的字段。但这种机制并不一定始终有效。
考虑你有一个函数,该函数接受一个对象作为参数,函数会使用instanceof运算符,检查这个对象是否是某一个类的实例。此时,可能会有一些问题。即使这个对象可能含有与特定类相同的字段,但除非这个对象真正继承了该类,否则,这种判断就会失败。
class Person {
constructor( name ) {
this.name = name;
}
}
class Employee {
constructor( name, salary ) {
this.name = name;
this.salary = salary;
}
}
// get name of an `object` of the type `Person`
function getName( object ) {
if( object instanceof Person ) {
console.log( 'success ->', object.name );
} else {
console.log( 'error -> not a Person' );
}
}
// create a `Person` object and get name
const person = new Person( 'Ross' );
getName( person ); // success -> Ross
// create an `Employee` object and get name
const employee = new Employee( 'Monica', 300 );
getName( employee ); // error -> not a Person 在上面的例子中,employee对象并不是Person类的实例,Employee类也没有继承Person类,因此,instanceof运算符返回false。为了解决这一问题,你需要增加OR条件,检查object对象是否是Employee类的实例。
通过给类增加一个名为Symbol.hasInstance的static函数,我们可以改变instanceof运算符的默认行为。你所要做的,就是检查传入的对象,然后返回一个boolean值。看下面的代码。
class Person {
constructor( name ) {
this.name = name;
}
static [ Symbol.hasInstance ] ( instance ) {
return 'name' in instance;
}
}
class Employee {
constructor( name, salary ) {
this.name = name;
this.salary = salary;
}
}
// get name of an `object` of the type `Person`
function getName( object ) {
if( object instanceof Person ) {
console.log( 'success ->', object.name );
} else {
console.log( 'error -> not a Person' );
}
}
// create a `Person` object and get name
const person = new Person( 'Ross' );
getName( person ); // success -> Ross
// create an `Employee` object and get name
const employee = new Employee( 'Monica', 300 );
getName( employee ); // success -> Monica 当<LHS> instanceof Person进行计算时,Person[Symbol.hasInstance]函数会被调用,函数参数是LHS。在上面的例子中,我们使用in运算符,检查name属性是否存在于instance或者其原型链上。这么做的理由是,只要对象具有name属性,我们就认为这就是一个Person对象。
Symbol.isConcatSpreadable
你一定使用过Array的原型函数[].concat(),由一个或多个元素创建一个新的数组。concat函数的神奇之处在于,如果参数类型是Array,那么这个参数会被扁平化。问题在于,有时候你可能并不想这么做。
你可以通过设置数组(Aarry的实例)的Symbol.isConcatSpreadable属性来改变这一行为。如果该属性存在,并且被设置为false,那么,concat函数就不会将数组类型的参数扁平化。
// declare custom Array class
class MyArray extends Array {
square() {
return this.map( i => i * i );
}
// do not spread `MyArray` values in `concat`
get [ Symbol.isConcatSpreadable ]() {
return false;
}
}
const numbers = new MyArray( 5, 33 );
// custom non-spreadable array
const drivers = [ "Seb", "Max" ];
drivers[ Symbol.isConcatSpreadable ] = false;
// simple arrays
const cars = [ "Ferrari", "RedBull" ];
const sports = [ "F1", "MotoGP" ];
// use concat
const newArray = cars.concat( sports, drivers, numbers );
console.log( newArray ); // ["Ferrari", "RedBull", "F1", "MotoGP", ["Seb", "Max", [Symbol(Symbol.isConcatSpreadable)]: false], MyArray [5, 33] ] 在上面的例子中,numbers和drivers属性并不会在concat运算中被扁平化,因此,在newArray中,它们仍然是数组值。但是,sports属性被展开了,因为它或其原型链上没有设置Symbol.isConcatSpreadable属性。
Symbol.species
在上面的例子中,我们定义了MyArray类。这个类扩展了内置的Array类。MyArray唯一改变的是Symbol.isConcatSpreadable属性。从技术上说,MyArray的所有实例都继承了Array的所有属性。
但是,现在有一个问题。如果我们对MyArray实例调用Array类的任意返回新的数组的原型函数,比如map、forEach等,结果都是返回MyArray的实例。这应该是可预期的结果。
// declare custom Array class
class MyArray extends Array {
square() {
return this.map( i => i * i );
}
}
// `numbers` of type `MyArray`
const numbers = new MyArray( 3, 5 );
console.log( 'numbers ->', numbers ); // numbers -> MyArray [ 3, 5 ]
// `squares` of type `MyArray`
const squares = numbers.square();
console.log( 'squares ->', squares ); // squares -> MyArray [ 9, 25 ]
// `cubes` of type `MyArray`
const cubes = numbers.map( i => i * i * i );
console.log( 'cubes ->', cubes ); // cubes -> MyArray [ 27, 125 ] 然而,有些时候,你可能只想为基类增加一些额外的行为,但是需要保留底层基类的核心功能。例如,我们想要在MyArray实例调用map、forEach等操作时,返回值还是Array实例。
Symbol.species是一个类的static属性,指向用于子类的构造函数,例如在Array例子中的map或forEach函数。
// declare custom Array class
class MyArray extends Array {
square() {
return this.map( i => i * i );
}
static get [ Symbol.species ]() {
return Array;
}
}
// `numbers` of type `MyArray`
const numbers = new MyArray( 3, 5 );
console.log( 'numbers ->', numbers ); // numbers -> MyArray [ 3, 5 ]
// `squares` of type `Array`
const squares = numbers.square();
console.log( 'squares ->', squares ); // squares -> [ 9, 25 ]
// `cubes` of type `Array`
const cubes = numbers.map( i => i * i * i );
console.log( 'cubes ->', cubes ); // cubes -> [ 27, 125 ] 上面的代码中,隐式或显式调用map函数,返回值类型都是Array,而不是MyArray。
这一特性不仅适用于Array,同样适用于Set、Map、RegExp、Promise、TypedArray以及ArrayBuffer。
正则表达式方法
现在,我们说起正则表达式,基本就是RegExp实例。我们也可以通过/.../字面量表达式的形式来创建正则表达式。这种正则表达式对象可以作用于字符串,用来匹配文本模式。
'google.com'.match(/^[a-z]+\.com$/gi) // google.com
从 ES2015 起,JavaScript 允许使用任意对象实现类似正则表达式的行为。因此,你可以将任意对象传递给str.match()函数,该函数会将这个对象当作正则表达式去匹配。这种对象需要实现一些预定义的函数,这些函数会在匹配时被调用。
当一个对象实现了Symbol.match函数,这个对象就可以用于String.prototype.match()函数。在获取str.match()的结果时,这个符号函数会被调用。
// defines a matcher object to `match` operation
class Matcher {
constructor( name, ends ) {
this.name = name.toUpperCase();
this.ends = ends;
}
// `String.prototype.match()` processor
[ Symbol.match ]( text ) {
const pattern = this.ends + this.name + this.ends;
return text.match( new RegExp( pattern, 'gi' ) );
}
}
// create a matcher to match `__EMAIL__` pattern
const matcher = new Matcher( 'email', '__' );
/*------*/
// sample text
const text = 'Enter your email address here: __EMAIL__ and provide an alternate email here: __EMAIL__';
console.log( text.match( matcher ) ); // [ '__EMAIL__', '__EMAIL__' ] 上面的例子,我们创建了Matcher类,这个类实现了Symbol.match实例函数,因此,matcher对象就可以使用这个函数。当调用text.match(matcher)函数时,Symbol.match就会被执行,其参数是text对象。我们可以在Symbol.match函数内部使用text对象,来返回一个合理的值。
我们还有Symbol.matchAll函数,用于处理String.prototype.matchAll()的调用。
当String实例调用search原型函数时,Symbol.search函数就会被调用。类似于Symbol.match,这个函数也接受调用search函数的字符串作为参数。
// defines a matcher object for `search` operation
class Matcher {
constructor( name, ends ) {
this.name = name.toUpperCase();
this.ends = ends;
}
// `String.prototype.search()` processor
[ Symbol.search ]( text ) {
const pattern = this.ends + this.name + this.ends;
return text.indexOf( pattern );
}
}
// create a matcher to find index of `__EMAIL__` pattern
const matcher = new Matcher( 'email', '__' );
/*------*/
// sample text
const text = 'Enter your email address here: __EMAIL__ and provide an alternate email here: __EMAIL__';
console.log( text.search( matcher ) ); // 31 当String实例调用replace原型函数时,Symbol.replace函数就会被调用。由于replace函数需要替换字符串,这个函数同样需要两个参数:原始字符串和替换字符串。函数需要返回一个字符串。
// defines a matcher object for `replace` operation
class Matcher {
constructor( name, ends ) {
this.name = name.toUpperCase();
this.ends = ends;
}
// `String.prototype.replace()` processor
[ Symbol.replace ]( text, replacement ) {
const pattern = this.ends + this.name + this.ends;
const matchIndex = text.indexOf( pattern );
return text.substring( 0, matchIndex ) + `"${ replacement }"` + text.substring( matchIndex + pattern.length, text.length );
}
}
// create a matcher to replace `__EMAIL__` pattern
const matcher = new Matcher( 'email', '__' );
/*------*/
// sample text
const text = 'Enter your email address here: __EMAIL__.';
console.log( text.replace( matcher, '*@*.com' ) ); // Enter your email address here: *@*.com 当String实例调用split原型函数时,Symbol.split函数就会被调用。该函数的参数是调用了split函数的字符串,返回值是一个数组。
// defines a matcher object for `split` operation
class Matcher {
constructor( name, ends ) {
this.name = name.toUpperCase();
this.ends = ends;
}
// `String.prototype.split()` processor
[ Symbol.split ]( text ) {
const pattern = this.ends + this.name + this.ends;
const matchIndex = text.indexOf( pattern );
return [
text.substring( 0, matchIndex ),
text.substring( matchIndex + pattern.length, text.length )
];
}
}
// create a matcher to split at `__EMAIL__` pattern
const matcher = new Matcher( 'email', '__' );
/*------*/
// sample text
const text = 'Enter your email address here: __EMAIL__.';
console.log( text.split( matcher ) ); // [ 'Enter your email address here: ', '.' ] Symbol.iterator
我们曾经在别的文章中介绍过Symbol.iterator。现在来总结一下。ES2015 引入了新的遍历协议,其中包含了使对象可遍历的一些准则。可遍历对象可以使用for-of循环,或者使用展开运算符。
到目前为止,我们只能使用Array作为可遍历的。为了遍历Object对象,需要使用for-in循环。而且,我们也不能原生展开一个对象,Object.values()或Object.entrues()或许可以帮到你。
利用新的遍历协议,我们可以像数组一样,使用for-of循环对象,或者展开对象。此时,JavaScript 会通过调用可遍历对象的Symbol.iterator函数获取一个遍历器。
Symbol.iterator函数会在遍历开始时被调用,其返回值是一个遍历器对象。这个遍历器有一个next函数,该函数返回一个对象,这个对象有一个boolean类型的done字段和value字段。
var obj = { max: 5 };
obj[Symbol.iterator] = function() {
let max = this.max; // `this` here is the `obj`
return {
next: () => {
return { done: 0 === max, value: max-- };
}
};
}
console.log( [ ...obj ] ); // [5, 4, 3, 2, 1] 在循环期间,iterator.next()函数会被一直调用,直到done为false。value字段是我们感兴趣的,代表每一次循环中的当前值。当done为false时,value被忽略。
可以在MDN文档中了解更多关于遍历协议的相关内容。
设计遍历器并不是一件多么令人愉快的事情,因为会有很多重复的代码。这也就是为什么,JavaScript 还提供了另外一个被称为生成器的机制。生成器是一种特殊的函数。当调用生成器函数(也就是生成器)时,会返回一个遍历器。因此,Symbol.iterator同样适用于生成器。
由生成器返回的遍历器的next函数被调用时,每一个yield表达式都会被求值,产生一个包含done和value的遍历对象。一旦所有的yield表达式都被求值,无论何时再次调用next函数,都会返回一个done被设置为true的遍历对象。
class Person {
constructor( name, favs ) {
this.name = name;
this.favs = favs;
}
// implement iterator protocol using a generator
*[ Symbol.iterator ]() {
for( let i in this.favs ) {
yield `${ this.name } likes ${ this.favs[ i ] }.`;
}
}
}
const person = new Person( 'Ross', [
'Monkeys', 'Museums', 'Rocks', 'Music'
] );
// iterate over `person` using `for-of` loop
for( let item of person ) {
console.log( 'item ->', item );
}
// iterate over `person` using spread operator
console.log( 'favs -> ', [ ...person ] );
// --> output
// item -> Ross likes Monkeys.
// item -> Ross likes Museums.
// item -> Ross likes Rocks.
// item -> Ross likes Music.
// favs -> [
// 'Ross likes Monkeys.',
// 'Ross likes Museums.',
// 'Ross likes Rocks.',
// 'Ross likes Music.',
//] *[Symbol.iterator]提供了一个生成器,因此我们需要在方法前面添加*。JavaScript 默认给很多类实现了遍历器协议,例如Array、Set、Map、String、TypedArray等。
typeof Array.prototype[Symbol.iterator]; // 'function' typeof String.prototype[Symbol.iterator]; // 'function' typeof Map.prototype[Symbol.iterator]; // 'function' typeof Set.prototype[Symbol.iterator]; // 'function' typeof Uint8Array.prototype[Symbol.iterator]; // 'function' // hence you can spread a string let vowels = [...'aeiou']; // [ 'a', 'e', 'i', 'o', 'u' ]
Symbol.asyncIterator
JavaScript 提供了一种for-of的变体,用于以同步的方式循环Promise对象。这种for-await-of循环可以以同步的方式遍历可遍历对象,每次遍历返回一个Promise对象。
function makePromise( time ) {
return new Promise( function( resolve ) {
setTimeout(() => {
resolve( `Resolved after: ${ time } ms.` );
}, time );
} );
}
var promises = [
makePromise( 1000 ),
makePromise( 800 ),
makePromise( 2000 ),
makePromise( 1500 ),
];
( async function() {
for await ( let result of promises ) {
console.log( 'result ->', result );
}
} )();
// --> output
// result -> Resolved after: 1000ms.
// result -> Resolved after: 800ms.
// result -> Resolved after: 2000ms.
// result -> Resolved after: 1500ms. 上面的例子中,promises包含一组Promise对象,每个promise会在几毫秒后返回一个字符串。如果我们使用普通的for-of循环,每次遍历都会接受到一个promise作为值。但是,我们所需要的其实是每个promise所持有的那个字符串。
for await语法正是解决这一问题。它会等待直到每个promise都完成,result变量的值是promise接受的值。由于我们使用了await关键字,因此for-await-of循环必须放在async函数中,这可以是一个立即调用的函数表达式 IIFE。
正如for-of循环那样,for-await-of循环也会使用Symbol.iterator函数获取遍历器。如果遍历器返回promise对象,其结果就像上面描述的那样。
然而,for-await-of循环更推荐使用Symbol.asyncIterator函数。该函数与Symbol.iterator类似,但是一个async函数,你可以在这个函数中使用await关键字。
function makePromise( time, fav ) {
return new Promise( function( resolve ) {
setTimeout(() => {
resolve( `${ fav }: ${ time } ms.` );
}, time );
} );
}
class Person {
constructor( name, favs ) {
this.name = name;
this.favs = favs;
}
// implement async iterator protocol using a generator
async *[ Symbol.asyncIterator ]() {
for( let i in this.favs ) {
yield await makePromise( i * 500, this.favs[ i ] );
}
}
}
const person = new Person( 'Ross', [
'Monkeys', 'Museums', 'Rocks', 'Music'
] );
// iterate over `person` using `for-of` loop
( async () => {
for await ( let item of person ) {
console.log( 'item ->', item );
}
} )();
// --> output
// item -> Monkeys: 0 ms.
// item -> Museums: 500 ms.
// item -> Rocks: 1000 ms.
// item -> Music: 1500 ms. 在上面的例子中,每一个yield表达式都会等待promise执行完毕,这样它就可以在遍历中获取promise接受的值。但是,你也可以直接返回promise本身,不使用await关键字,其效果与带有await一致。
什么是@@iterator?
在 JavaScript 文档中,比如 MDN Array 文档,或者在控制台或堆栈记录中,你可能看到过@@iterator标记。@@记号是一种特殊的前缀,用于表示预定义符号,这种记号并不会出现在运行时。
下面的表格是 ES2021 定义的预定义符号。左列是这些符号的标准名字,右列是运行时可访问到的实际符号。
| 标准名字 | [[Description]] |
@@asyncIterator | "Symbol.asyncIterator" |
@@hasInstance | "Symbol.hasInstance" |
@@isConcatSpreadable | "Symbol.isConcatSpreadable" |
@@iterator | "Symbol.iterator" |
@@match | "Symbol.match" |
@@matchAll | "Symbol.matchAll" |
@@replace | "Symbol.replace" |
@@search | "Symbol.search" |
@@species | "Symbol.species" |
@@split | "Symbol.split" |
@@toPrimitive | "Symbol.toPromitive" |
@@toStringTag | "Symbol.toStringTag" |
@@unscopables | "Symbol.unscopables" |
Symbol.unscopables用于控制对象在with语句内的行为。但是因为with语句并不推荐使用,因此这个函数也应该避免。
这些符号的好处是什么?
这是这篇文章中最难写的部分。简单来说,这些符号能够避免 API 被错误的使用。例如,如果不想让别人意外覆盖某个对象属性,就可以将其声明为一个符号,然后通过全局对象暴露,或者作为全局符号。
另外,符号特别适合表达为唯一值。例如,如果你创建了唯一值的枚举,就可以使用符号