JS - 强大的 Symbol 内置属性

195 阅读4分钟

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