ES 引入了内置符号(都以 Symbol 工厂函数字符串属性的形式存在)用于暴露语言内部的行为,开发者可以直接访问、重写或模拟这些行为,比如
- 重新定义,使用 Symbol.iterator 来改变 for-of 在迭代该对象的行为
- 直接访问,ECMAScript 在引用符号规范中名字前缀为 @@,比如 @@iterator 就是 Symbol.iterator 能直接访问的原因是这些内置符号就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例,所有的内置符号属性都是不可写、不可枚举、不可配置的
注意 ⚠️: 许多 String.prototype.[method] 的方法既可以传递正则也可以传递 string 类型的值,但是通常其内部会 将 string 类型的值转化为正则。
Symbol.asyncIterator 异步迭代器供使用 for-await-of
for-await-of 循环会利用Symbol.asyncIterator 提供的函数执行异步迭代操作,并期待这个函数会返回一个实现迭代器 API 的对象(AsyncGenerator),这个对象应该通过其 next() 方法陆续返回 Promise 实例,可以通过显示或隐式地调用 next() 返回 Promise 实例
class Emitter {
constructor(max) {
this.max = max;
this.asyncIdx = 1;
}
// Symbol.asyncIterator 作为 一个 async Generator 的函数名
async *[Symbol.asyncIterator]() {
while (this.asyncIdx <= this.max) {
// yield 相当于 next() 方法,返回 Promise 实例,累加 asyncIdx
yield new Promise((resolve) => resolve(this.asyncIdx++));
}
}
}
const print0ToMax = async (max) => {
const emitter = new Emitter(max);
// await 用于 async 内部,因此这里需要使用一个 async 函数包裹一下, 不然会报错
for await (const x of emitter) {
console.log(x);
}
};
print0ToMax(5); // 依次换行打印: 1,2,3,4,5
Symbol.iterator 供 for-of 使用
和 Symbol.asyncIterator 一样, Symbol.iterator 需要返回一个迭代器供 for-of 语句使用,即 for-of 循环会利用 Symbol.iterator为键的函数返回的 迭代器对象(大多数时候是 Generator)
const { log } = console;
class SIteratorEmitter {
constructor(max) {
this.max = max;
this.idx = 0;
}
// Symbol.iterator 作为一个 Generator 函数名
*[Symbol.iterator]() {
while (this.idx <= this.max) {
yield this.idx++;
}
}
}
const print0ToMax = (max) => {
const emitter = new SIteratorEmitter(max);
for (const x of emitter) {
console.log(x);
}
};
print0ToMax(5); // 0,1,2,3,4,5
Symbol.hasInstance 供 instance 使用
ES6 中,instanceof 使用 Symbol.hasInstance 函数来确定关系,这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用(可以借助这一点来在类上定义静态方法实现重写该判断)。默认实现了一个对象是否属于构造对象的实例的判断,因此如下两种方法等效
// 构造函数
function Foo() {}
class FooClass {}
const foo = new Foo();
const fooClass = new FooClass();
const { log } = console;
log(foo instanceof Foo); // true;
log(Foo[Symbol.hasInstance](foo)); // true;
log(fooClass instanceof FooClass); // true;
log(FooClass[Symbol.hasInstance](fooClass)); // true;
// foo 不是 FooClass 的实例
log(FooClass[Symbol.hasInstance](foo)); // false;
Symbol.isConcatSpreadable 供 Array.prototype.concat(likeArr) 使用
Symbol.isConcatSpreadable 如果为 true, 则会将拥有 Symbol.isConcatSpreadable 的类数组(likeArrObj)在调用 arr.concat(likeArrObj)根据其 length 属性和数字索引 key 转化为数组并做合并操作;如果不是类数组或数组,则将会忽略传递进来的值,默认 Symbol.isConcatSpreadable 为 undefined)
const { log } = console;
const callArr = [1, 2]; // 调用 concat 的数组
/**
* 类数组的定义:有用 length 属性
* 这里的类数组长度为 3, 转化为数组之后 [3,4,[5,6]];
*/
const likeArrObj = {
length: 3,
0: 3,
1: 4,
2: [5, 6],
};
// 即不是数组,也不是非数组
const noneArr = new Set([1, 2]); // Set(2) { 1, 2 }
log(callArr[Symbol.isConcatSpreadable]); // log: undefined 默认值
log(likeArrObj[Symbol.isConcatSpreadable]); // log: undefined 默认值
// 直接 concat 类数组, 并没有将 类数组转化为 数组后进行 concat
log(callArr.concat(likeArrObj)); // log: [ 1, 2, { '0': 3, '1': 4, '2': [ 5, 6 ], length: 3 } ]
// 设置 类数组对象的 Symbol.isConcatSpreadable 属性为 true
likeArrObj[Symbol.isConcatSpreadable] = true;
// 在将 类数组对象传递给 concat 作为实参 的时候会根据 length 转化为 数组
log(callArr.concat(likeArrObj)); // log: [ 1, 2, 3, 4, [ 5, 6 ] ]; 这就是 魔法
// 给 非类数组设置 Symbol.isConcatSpreadable 属性设置为 true 会在调用 concat 的时候被忽略
noneArr[Symbol.isConcatSpreadable] = true;
log(callArr.concat(noneArr)); // log: [ 1, 2 ], 看 非数组设置了根本不管用
Symbol.match 供 String.prototype.match(reg/str) 使用
Symbol.match 提供的方法用于正则表达式匹配字符串,由 String.prototype.match(reg) 方法使用,正则表达式实例默认实现了 String 方法的定义和实现(默认实现传递给 match 的不是正则表达式,会被转换为正则表达式)
Symbol.match 函数接受一个参数,就是调用 match() 方法的字符串实例
const { log } = console;
class StrMatcher {
// str 为 要传递给要匹配的内容
constructor(str) {
this.str = str;
}
// source 为 调用 match 的源字符
[Symbol.match](source) {
return source.includes(this.str);
}
}
log("foobar".match(new StrMatcher("foo"))); // true;
log("foobar".match(new StrMatcher("xxx"))); // false;
Symbol.replace 供 String.prototype.replace(reg/str) 使用
Symbol.replace 提供的方法会被 String.prototype.replace(reg) 使用,replace 会使用传递的入参来匹配调用它的字符串并替换掉匹配上的子串。
Symbol.replace 方法接受两个参数,即调用 replace() 方法的字符串实例和替换字符串,返回值没有限制
const { log } = console;
// replace 方法原来的使用
log("hello world".replace("hello", "my")); // log: my world; 这里传递的 hello 字符串会被转换为 正则对象
log("hello world".replace(/hello/, "my")); // log: my world
class StaticReplacer {
/**
* 使用 Symbol.replace 来覆盖 String.prototype.replace 方法
* @param {*} target 调用 replace 方法的字符串
* @param {*} replacement 替换的字符串
*/
static [Symbol.replace](target, replacement) {
return target.replace(replacement, `$${replacement}$`);
}
}
log("hello".replace(StaticReplacer, "el")); // h$el$lo
class StrReplacer {
constructor(str) {
this.str = str;
}
// 这个方法里边可以任意写你的实现,即使它没有意义
[Symbol.replace](target, replacement) {
return `${target} ${this.str} ${replacement}`;
}
}
log("hello".replace(new StrReplacer("my"), "world")); // log: hello my world
Symbol.search 供 String.prototype.search(reg/str) 使用
String.prototype.search(reg/str) 方法返回字符串中匹配上正则表达式的索引,因此可以通过 Symbol.search 重写该方法
const { log } = console;
// 传递给 search 的参数值会 被转换为 正则对象
log("search box".search("box")); // log: 7
log("search box".search(/box/)); // log: 7
class StaticSearcher {
// Symbol.search 接受的 参数就是 调用的字符串,里边你可以做任意的操作
static [Symbol.search](target) {
return target.indexOf("box");
}
}
log("search box".search(StaticSearcher)); // log: 7
log("1box".search(StaticSearcher)); // log: 1
class StrSearcher {
constructor(str) {
this.str = str;
}
[Symbol.search](target) {
return target.indexOf(this.str);
}
}
log("search box".search(new StrSearcher("box"))); // log: 7
log("1box".search(new StrSearcher("box"))); // log: 1
Symbol.search 供 String.prototype.search(reg/str) 使用
Symbol.search 可以重写 String.prototype.search(reg/str), 然后在后者调用的时候 使用 Symbol.search 提供的函数来调用
const { log } = console;
// 传递给 search 的参数值会 被转换为 正则对象
log("search box".search("box")); // log: 7
log("search box".search(/box/)); // log: 7
class StaticSearcher {
// Symbol.search 接受的 参数就是 调用的字符串,里边你可以做任意的操作
static [Symbol.search](target) {
return target.indexOf("box");
}
}
log("search box".search(StaticSearcher)); // log: 7
log("1box".search(StaticSearcher)); // log: 1
class StrSearcher {
constructor(str) {
this.str = str;
}
[Symbol.search](target) {
return target.indexOf(this.str);
}
}
log("search box".search(new StrSearcher("box"))); // log: 7
log("1box".search(new StrSearcher("box"))); // log: 1
Symbol.split 供 String.prototype.split(reg/str) 使用
Symbol.split 可以覆盖 String.prototype.split(reg/str) 的逻辑
const { log } = console;
class StaticSplitter {
// 以 Symbol.split 方法创建的 方法可以覆盖 String.prototype.split(reg/str) 方法
// target 就是调用 split 的字符串
static [Symbol.split](target) {
return target.split(",");
}
}
log("1,2,3,4,5".split(StaticSplitter)); // log: [ '1', '2', '3', '4', '5' ]
class Splitter {
constructor(str) {
this.str = str;
}
// target 就是调用 split 的字符串
[Symbol.split](target) {
return target.split(this.str);
}
}
log("1#2#3#4#5".split(new Splitter("#"))); // log: [ '1', '2', '3', '4', '5' ]
Symbol.toPrimitive 将对象转化为相应的原始值
Symbol.toPrimitive 将一个值转化为原始值, 很多内置操作都会尝试 使用 Symbol.toPrimitive 强制将对象转化为原始值(包括字符串、number 和 未指定的原始类型)
如下代码将会利用 Symbol.toPrimitive 来和一个对象进行计算
const { log } = console;
class Demo {}
const d = new Demo();
log(3 + d); // 3[object Object]
log(3 - d); // NaN 属于 number 类型
log(String(d)); // [object Object]
class PrimitiveOperator {
constructor() {
this[Symbol.toPrimitive] = function (type) {
const typeMap = {
number: 3,
string: "string",
default: " any",
};
return typeMap[type] || "nothing";
};
}
}
log("---- split ----");
const p = new PrimitiveOperator(3);
log(3 + p); // log: 3 any; 此时 p 的 type 对应为 default,因此结果为 3 any
log(3 - p); // log: 0; 此时 p 被 - 内置符号 转化为了 number 因此 3 - 3 = 0
log(String(p)); //log: string; p 被转化为了 string,因此 结果为 string
Symbol.toStringTag 供 Object.prototype.toString() 使用
通过 toString() 方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符(默认为 Object,内置类型已经指定了这个值),自定义实例需要通过 Symbol.toStringTag 明确定义
toString 方法 如果 obj 是对象类型,便会 返回 [object ${obj[Symbol.toStringTag]} || Object] 字符串值
const { log } = console;
const s = new Set();
log(s); // log: Set(0) {}
log(s.toString()); // log: [object Set]
log(s[Symbol.toStringTag]); // log: Set 内置已经实现了 Symbol.toStringTag 的返回
class Custom {}
const c = new Custom();
log(c); // log: Custom {}
log(c.toString()); // log: [object Object]
log(c[Symbol.toStringTag]); // log: undefined 需要实现 Symbol.toStringTag 的返回
class CustomTag {
constructor() {
this[Symbol.toStringTag] = "CustomTag";
}
}
const ct = new CustomTag();
log(ct); // log: CustomTag { [Symbol(Symbol.toStringTag)]: 'CustomTag' }
log(ct.toString()); // log: [object CustomTag]
log(ct[Symbol.toStringTag]); // log: CustomTag