ES6-Symbol-3

425 阅读10分钟

背景

前面几天学习了 Symbol 的基础使用,这次是学习 Symbol 深入的内容,Symbol 的内置值,加深对 Symbol 的了解。

Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo 在语言内部,实际调用的是 Foo[Symbol.hasInstance](foo)

Symbol.hasInstance 属性的属性特性: 特性值
writable false
enumerable false
configurable false

Symbol 内置值的属性特性是相同的,下面就不在列出了。

class MyClass {
  [Symbol.hasInstance](foo) {
    return foo instanceof Array;
  }
}

[1, 2, 3] instanceof new MyClass(); // true

上面代码中,MyClass 是一个类,new MyClass()会返回一个实例。该实例的 Symbol.hasInstance 方法,会在进行 instanceof 运算时自动调用,判断左侧的运算子是否为 Array 的实例。

Symbol.isConcatSpreadable

内置的 Symbol.isConcatSpreadable(@@isConcatSpreadable)符号用于配置某对象作为 Array.prototype.concat()方法的参数时是否展开其数组元素,这是一个布尔类型,它可以控制数组或类似数组(array-like)的对象的行为:

  • 对于数组对象,默认情况下,用于concat时,会按数组元素展开然后进行连接(数组元素作为新数组的元素)。重置 Symbol.isConcatSpreadable 可以改变默认行为。
  • 对于类似数组(Array-like)的对象,用于concat时,该对象整体作为新数组的元素,重置Symbol.isConcatSpreadable可改变默认行为。

Symbol.isConcatSpreadablel示例

上面可以看出,数组的默认行为是可以展开的,Symbol.isConcatSpreadable默认等于undefined。该属性等于true时,也有展开的效果。对于类数组,默认不展开,它的Symbol.isConcatSpreadable属性设为true,才可以展开,例如上面的likeArray,但是上面提出了一个问题,为啥没有设置length值之后就没有合并了呢,如果设置不同的length值会有啥影响,可以思考下哦。

Symbol.isConcatSpreadable属性也可以定义在类里面。

class A1 extends Array {
  constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = true;
  }
}
class A2 extends Array {
  constructor(args) {
    super(args);
  }
  get [Symbol.isConcatSpreadable]() {
    return false;
  }
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2);
// [1, 2, 3, 4, [5, 6]]

上面代码中,类A1是可展开的,类A2是不可展开的,所以使用concat时有不一样的结果。

注意,Symbol.isConcatSpreadable的位置差异,A1是定义在实例上,A2是定义在类本身,效果相同。

Symbol.iterator

Symbol.iterator为每一个对象定义了默认的迭代器,在对象进行for...of 循环时,会调用 Symbol.iterator方法,也就是它的@@iterator方法会在不传参情况下被调用,会返回的迭代器用于获取要迭代的值。 一些内置类型有默认的迭代器行为,下面的内置类型拥有默认的@@iterator方法:

我们自己也可以自定义迭代器:

var myIterable = {};
myIterable[Symbol.iterator] = function*() {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable]; // [1, 2, 3]
class Collection {
  *[Symbol.iterator]() {
    let i = 0;
    while (this[i] !== undefined) {
      yield this[i];
      ++i;
    }
  }
}

let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;

for (let value of myCollection) {
  console.log(value);
}
// 1
// 2

上面代码中,Generator函数调用之后会返回一个迭代器对象,所以ES6 扩展运算符(...)for...of可以进行操作和遍历。

Symbol.asyncIterator

Symbol.asyncIterator符号指定了一个对象的默认 AsyncIterator。如果一个对象设置了这个属性,它就是异步可迭代对象,可用于for await...of循环。 需要注意的一点是,目前没有默认设定了Symbol.asyncIterator属性的JavaScript内建的对象,所以如果使用需要自定义异步可迭代对象。

下面先看一段代码:

const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
  yield "hello";
  yield "async";
  yield "iteration!";
};

(async () => {
  for await (const x of myAsyncIterable) {
    console.log(x);
    // expected output:
    //    "hello"
    //    "async"
    //    "iteration!"
  }
})();

上面代码中,异步迭代器需要返回一个promise,所以使用async定义,如果你遇到了 SyntaxError: for await (... of ...) is only valid in async functions and async generators 错误,那是因为 for-await-of 只能在 async 函数或者 async 生成器里面使用。

Symbol 可以定义同步迭代器和异步迭代器,关于他们的使用和区别,我也做了学习和比较,详细了解可以阅读我的另一篇Iterator & asyncIterator,本文主要聚焦Symbol的内置对象,就不做详细介绍了。

Symbol.match

对象的Symbol.match属性,指向一个函数,当执行 str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

String.prototype.match(regexp);
// 等同于
regexp[Symbol.match](this);

class MyMatcher {
  [Symbol.match](string) {
    return "hello world".indexOf(string);
  }
}

"e".match(new MyMatcher()); // 1

对象的Symbol.match属性还可用于标识对象是否具有正则表达式的行为。比如, String.prototype.startsWith()String.prototype.endsWith()String.prototype.includes() 这些方法会检查其第一个参数是否是正则表达式,是正则表达式就抛出一个 TypeError。现在,如果match symbol 设置为 false(或者一个 假值),就表示该对象不打算用作正则表达式对象。

上面我们可以看出,修改reg的属性为 false 之后,String.prototype.startsWith()reg当成字符串来进行处理。

Symbol.matchAll

Symbol.matchAll 返回一个迭代器,该迭代器根据字符串生成正则表达式的匹配项。此函数可以被String.prototype.matchAll()方法调用。

let re = /[0-9]+/g;
let str = "2016-01-02|2019-03-07";
let result = re[Symbol.matchAll](str);

console.log(Array.from(result, x => x[0]));
// expected output: Array ["2016", "01", "02", "2019", "03", "07"]

Symbol.replace

Symbol.replace 这个属性指定了当一个字符串替换所匹配字符串时所调用的方法。String.prototype.replace() 方法会调用此方法。

更多信息, 详见 RegExp.prototype[@@replace]()String.prototype.replace()

String.prototype.replace(searchValue, replaceValue);
// 等同于
searchValue[Symbol.replace](this, replaceValue);

//例子
const x = {};
x[Symbol.replace] = (...s) => console.log(s);

"Hello".replace(x, "World"); // ["Hello", "World"]

Symbol.replace方法会收到两个参数,第一个参数是replace方法正在作用的对象,上面例子是Hello,第二个参数是替换后的值,上面例子是World

Symbol.search

对象的Symbol.search属性,指向一个方法,当该对象被 String.prototype.search 方法调用时,会返回该方法的返回值。

String.prototype.search(regexp);
// 等同于
regexp[Symbol.search](this);

class MySearch {
  constructor(value) {
    this.value = value;
  }
  [Symbol.search](string) {
    return string.indexOf(this.value);
  }
}
"foobar".search(new MySearch("foo")); // 0

Symbol.species

Symbol.species 是个函数值属性,其被构造函数用以创建派生对象。

class MyArray extends Array {}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);

b instanceof MyArray; // true
c instanceof MyArray; // true

上面代码中,子类MyArray继承了父类ArrayaMyArray的实例,bca的衍生对象。你可能会认为,bc都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例。

Symbol.species属性就是为了解决这个问题而提供的。现在,我们可以为MyArray设置Symbol.species属性。

class MyArray extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

上面代码中,由于定义了Symbol.species属性,创建衍生对象时就会使用这个属性返回的函数,作为构造函数。这个例子也说明,定义Symbol.species属性要采用get取值器。默认的Symbol.species属性等同于下面的写法。

static get [Symbol.species]() {
  return this;
}

现在,再来看前面的例子。

class MyArray extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

const a = new MyArray();
const b = a.map(x => x);

b instanceof MyArray; // false
b instanceof Array; // true

上面代码中,a.map(x => x)生成的衍生对象,就不是MyArray的实例,而直接就是Array的实例。

再看一个例子。

class T1 extends Promise {}

class T2 extends Promise {
  static get [Symbol.species]() {
    return Promise;
  }
}

new T1(r => r()).then(v => v) instanceof T1; // true
new T2(r => r()).then(v => v) instanceof T2; // false

上面代码中,T2定义了Symbol.species属性,T1没有。结果就导致了创建衍生对象时(then方法),T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。

总之,Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。

Symbol.split

对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

String.prototype.split(separator, limit);
// 等同于
separator[Symbol.split](this, limit);

class MySplitter {
  constructor(value) {
    this.value = value;
  }
  [Symbol.split](string) {
    let index = string.indexOf(this.value);
    if (index === -1) {
      return string;
    }
    return [string.substr(0, index), string.substr(index + this.value.length)];
  }
}

"foobar".split(new MySplitter("foo"));
// ['', 'bar']

"foobar".split(new MySplitter("bar"));
// ['foo', '']

"foobar".split(new MySplitter("baz"));

上面方法使用Symbol.split方法,重新定义了字符串对象的split方法的行为。

Symbol.toPrimitive

Symbol.toPrimitive是一个内置的Symbol值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。

Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

  1. Number:该场合需要转成数值
  2. String:该场合需要转成字符串
  3. Default:该场合可以转成数值,也可以转成字符串
let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case "number":
        return 123;
      case "string":
        return "str";
      case "default":
        return "default";
      default:
        throw new Error();
    }
  }
};

2 * obj; // 246
3 + obj; // '3default'
obj == "default"; // true
String(obj); // 'str'

Symbol.toStringTag

对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object][object Array]object后面的那个字符串。

  • 许多内置的 JavaScript 对象类型即便没有 toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签,比如:

    Object.prototype.toString.call("foo"); // "[object String]"
    Object.prototype.toString.call([1, 2]); // "[object Array]"
    Object.prototype.toString.call(3); // "[object Number]"
    Object.prototype.toString.call(true); // "[object Boolean]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    Object.prototype.toString.call(null); // "[object Null]"
    
  • 另外一些对象类型则不然,toString() 方法能识别它们是因为引擎为它们设置好了 toStringTag 标签,ES6 有内置对象的Symbol.toStringTag属性值如下:

    • JSON[Symbol.toStringTag]:'JSON'
    • Math[Symbol.toStringTag]:'Math'
    • Module 对象 M[Symbol.toStringTag]:'Module'
    • ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer'
    • DataView.prototype[Symbol.toStringTag]:'DataView'
    • Map.prototype[Symbol.toStringTag]:'Map'
    • Promise.prototype[Symbol.toStringTag]:'Promise'
    • Set.prototype[Symbol.toStringTag]:'Set'
    • %TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array'等
    • WeakMap.prototype[Symbol.toStringTag]:'WeakMap'
    • WeakSet.prototype[Symbol.toStringTag]:'WeakSet'
    • %MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator'
    • %SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator'
    • %StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator'
    • Symbol.prototype[Symbol.toStringTag]:'Symbol'
    • Generator.prototype[Symbol.toStringTag]:'Generator'
    • GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'
  • 可以给自己的类加上toStringTag属性,这样就可以有自己的自定义标签了

    class MyClass {
      get [Symbol.toStringTag]() {
        return "MyClass";
      }
    }
    
    Object.prototype.toString.call(new MyClass()); // "[object MyClass]"
    

Symbol.unscopables

对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用 with 关键字时,哪些属性会被with环境排除。

这里面讲到了一个with,感兴趣的同学可以查下,with本人工作中用的比较少,虽然可以简化代码,但是却会带来内存泄漏风险和性能下降等问题,所以也不建议使用。

偏题了,下面讲Symbol.unscopables,先来看个例子:

Array.prototype
上面的代码说明,Array 数组有 11 个属性,会被with排除。

我们可以通过给对象设置Symbol.unscopables对象来设置with排除对象。

// 没有 unscopables 时
class MyClass {
  foo() {
    return 1;
  }
}

var foo = function() {
  return 2;
};

with (MyClass.prototype) {
  foo(); // 1
}

// 有 unscopables 时
class MyClass {
  foo() {
    return 1;
  }
  get [Symbol.unscopables]() {
    return { foo: true };
  }
}

var foo = function() {
  return 2;
};

with (MyClass.prototype) {
  foo(); // 2
}

上面这个例子中,在没有指定foo函数为with排除对象时,with语法块会先在MyClass.prototype内部查找,当设置foo被排出后,就会提升作用域去寻找foo属性,则此时的foo指向外层作用域的变量。

总结

Symbol的学习到此就基本结束了,学习的内容很多,也恶补了很多,真的学习了好多天,需要不断去尝试,但是依然还有很多需要去学习,学无止境,继续加油,over~~~