Symbol常用内置符号

1,143 阅读12分钟

引子

本文讲述了 Symbol 常用内置符号的自定义方法与使用场景附代码演示 (难理解的知识点有配带图文)

常用内置符号

ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。

内置符号介绍:

  • 这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。
  • 这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为;
  • 这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例
  • 所有内置符号属性都是不可写(writable: false )、不可枚举(enumerable: false)、不可配置的(configurabe: false )

注意: 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@

比如,@@iterator 指的就是 Symbol.iterator

下面我们来讲一下 Symbol 中常用的内置方法:

 

Symbol.Iterator

@@Iterator 符号属性用于“为每一个对象定义默认的迭代器”,可以说这个符号表示实现迭代器 API 的函数;由 for-of 循环语句使用,循环时,它们会调用以 @@iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象,但有些时候,返回的对象是实现该 API的 Generator

自定义迭代器

若一个function声明后面跟着*,则表示这个函数为 Generator(生成器) 函数

简单使用迭代器,如下所示:

 let myIterable = {}
 myIterable[Symbol.iterator] = function* (){
   yield 1;
   yield 2;
   yield 3;
 }
 ​
 console.log([...myIterable]);  // [1,2,3]
 console.log(myIterable);  // {Symbol(Symbol.iterator): ƒ* ()}

常规使用迭代器

这个是常用迭代器的代码方案,如下图所示:

代码解释

  1. 定义一个类,使用constructor构造函数返回实例对象this(使用new创建对象实例时会自己调用该方法),定义了表示最大值的属性值max 与 表示为当前下标的属性值idx
  2. 创建一个迭代器,用于执行迭代过程,语法为每当idx小于max,yield迭代传递 idx++
  3. 创建一个基本函数,内部引用构造函数创建实例对象,传入的值表示max5,最后循环输入idx的值

基本的构造函数实例对象对于迭代器的使用大致如此,可以举一反三自己写出的一套模板  

Symbol.asyncIterator

@@asyncIterator 符号属性用于“指定一个对象的默认异步迭代器”;如果一个对象设置了这个属性,它就是异步可迭代对象;由 for-await-of 循环语句使用,循环时,它们会调用以 @@asyncIterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象,但有些时候,返回的对象是实现该 API的 AsyncGenerator

自定义异步迭代器

通过使用 async 关键字异步声明来定义一个基础的迭代器,再通过 asyncawait 搭配使用出 Promise 的异步行为;如下所示:

 const asy = new Object();
 asy[Symbol.asyncIterator] = async function*() {
     yield "Hello";
     yield "Async";
     yield "Iteration";
 };
 ​
 // (fucntion()=>{})()为立即执行函数
 (async () => {
     for await (const x of asy) {
         console.log(x);
     }
 })();
 // Hello
 // Async
 // Iteration

常规使用迭代器

@@asyncIterator 函数生成的对象应该通过其 next() 方法陆续返回 Promise 实例;可以通过显示地调用 next() 方法返回,也可以隐式地通过异步生成器函数返回;如下图所示:

代码解释

  1. 定义一个类,使用constructor构造函数返回实例对象this(使用new创建对象实例时会自己调用该方法),定义了表示最大值的属性值max 与 表示为当前下标的属性值asyncIdx
  2. 使用async创建一个异步迭代器,用于执行异步迭代;内层语法为每当asyncIdx小于max,yield迭代返回一个Promise 实例实现异步过程iasyncIdx++
  3. 使用async创建一个异步执行函数,内部引用构造函数创建实例对象,传入的值表示max5,最后搭配await的执行异步操作循环输入asyncIdx的值

🥚ps: 这部分有点超纲,关于一些知识点不理解的可以略过,也可以查看我另一篇文章 Promise简述与用法 或是 权威的 asyncawaitconstructor  

Symbol.hasInstance

@@hasInstance符号属性用于“检测某个对象是否是某个构造器的实例”,由 instanceof 操作符使用;

instance基本使用

instanceof 操作符可以用来确定一个对象实例的原型链上是否有原型;

 function Foo() { }
 let f = new Foo();
 console.log(f instanceof Foo); // true
 ​
 class Bar { }
 let b = new Bar();
 console.log(b instanceof Bar); // true
 ​
 console.log(f instanceof Bar); // false

Symbol.hasInstance基本使用

ES6 中,instanceof 操作符会使用 @@hasInstance 函数来确定关系;

@@hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下,如下所示:

 function Foo() {}
 let f = new Foo();
 console.log(Foo[Symbol.hasInstance](f)); // true
 ​
 class Bar {}
 let b = new Bar();
 console.log(Bar[Symbol.hasInstance](b)); // true
 ​
 console.log(Bar[Symbol.hasInstance](f)); // false

这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用;

静态方法搭配使用

由于 instanceof 操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数,如下所示:

 class Bar {}
 class Two extends Bar {
   // 通过静态方法定义
   static [Symbol.hasInstance]() {
         return 111;
   }
 }
 ​
 let b = new Two();
 console.log(Bar[Symbol.hasInstance](b)); // true
 console.log(b instanceof Bar); // true
 console.log(Two[Symbol.hasInstance](b)); // false
 console.log(b instanceof Two); // false

 

Symbol.isConcatSpreadable

@@isConcatSpreadable 符号属性表示为“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat() 打平其数组元素”;

使用规则

ES6 中的 Array.prototype.concat() 方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例,覆盖 @@isConcatSpreadable 的值可以修改这个行为。

  • 数组对象默认情况下会被打平到已有的数组
  • false 或假值会导致整个对象被追加到数组末尾
  • 类数组对象默认情况下会被追加到数组末尾
  • true 或真值会导致这个类数组对象被打平到数组实例
  • 其他不是类数组对象的对象在 @@isConcatSpreadable 被设置为 true 的情况下将被忽略

代码演示

前言: coucat是一个数组的拼接方法;Set对象是值的集合,具有去重效果

  1. 这里创建了 test 与 array两个变量:

     let test = ["测试"];
     let array = ["数组"];
    
  2. 我们先试试数组场景:

     console.log(array[Symbol.isConcatSpreadable]); // undefined
     console.log(test.concat(array));  // [ '测试', '数组' ]
     `ps:设置为 false 表示后面添加的将整个对象追加到数组尾部`
     array[Symbol.isConcatSpreadable] = false;  
     console.log(test.concat(array));  // [ '测试', [ '数组', [Symbol(Symbol.isConcatSpreadable)]: false ] ]
    
  3. 再试试对象场景:

     let arrObject = { length: 1, 0: "MVP" };
     console.log(arrObject[Symbol.isConcatSpreadable]); // undefined
     console.log(test.concat(arrObject)); // [ '测试', { '0': 'MVP', length: 1 } ]
     `ps:设置为 true 表示导致这个类数组对象被打平到数组实例`
     arrObject[Symbol.isConcatSpreadable] = true;
     console.log(test.concat(arrObject)); // ['测试', 'MVP']
    
  4. 最后试试其他对象,这里用Set()集合演示:

     let otherObject = new Set().add("MVVM");
     console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
     console.log(test.concat(otherObject)); // [ '测试', Set(1) { 'MVVM' } ]
     otherObject[Symbol.isConcatSpreadable] = true;
     console.log(test.concat(otherObject)); // [ '测试' ]
    

 

Symbol.match

@@match 符号属性用于“检索返回一个字符串匹配正则表达式的结果”;由 String.prototype.match()方法使用,这个方法会使用以 @@match 为键的函数来对正则表达式求值。

代码演示

正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数

 console.log(RegExp.prototype[Symbol.match]);
 // ƒ [Symbol.match]() { [native code] }
 console.log('foobar'.match(/bar/));
 // ["bar", index: 3, input: "foobar", groups: undefined]

自定义

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象;如果想改变这种行为,让方法直接使用参数,则可以重新定义 @@.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。

@@match 函数接收一个参数,就是调用 match()方法的字符串实例,返回的值没有限制;如下所示:

 class FooMatcher {
     static [Symbol.match](target) {
         // String.includes 用于查找字符串是否包含此字符串,返回布尔值
         return target.includes('foo');
     }
 }
 ​
 console.log('foobar'.match(FooMatcher)); // true 
 console.log('barbaz'.match(FooMatcher)); // false 
 ​
 /* 重新定义Symbol.match() 函数 */
 class StringMatcher {
     constructor(str) {
         this.str = str;
     }
     [Symbol.match](target) {
         return target.includes(this.str);
     }
 }
 ​
 // String.match(new StringMatcher(String)) 
 // 第一个String为总字符串,第二个String为要查找的字符
 console.log('foobar'.match(new StringMatcher('foo'))); // true 
 console.log('barbaz'.match(new StringMatcher('qux'))); // false 

 

Symbol.replace

@@replace符号属性用于“替换一个字符串中匹配的子串”;由 String.prototype.replace()方法使用,这个方法会使用以 @@replace 为键的函数来对正则表达式求值。

代码演示

正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:

 console.log(RegExp.prototype[Symbol.replace]); 
 // ƒ [Symbol.replace]() { [native code] }
 console.log('TestAdd'.replace(/Add/,'Next'));
 // TestNext

自定义

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象;如果想改变这种行为,让方法直接使用参数,可以重新定义 @@replace 函数以取代默认对正则表达式求值的行为,从而让replace()方法使用非正则表达式实例;

@@replace 函数接收两个参数,即调用 replace()方法的字符串实例和替换字符串,返回的值没有限制;如下所示:

 class FooReplacer {
     static [Symbol.replace](target, replacement) {
         // split 为分隔 join为拼接
         return target.split('One').join(replacement);
     }
 }
 ​
 console.log('TestOneLP'.replace(FooReplacer, 'Two')); // TestTwoLP
 ​
 class StringReplacer {
     constructor(str) {
         this.str = str;
     }
     [Symbol.replace](target, replacement) {
         return target.split(this.str).join(replacement);
     }
 }
 ​
 console.log('TestOneXS'.replace(new StringReplacer('One'), 'Two')); // TestTwoXS

 

Symbol.search

@@search符号属性用于“返回字符串中匹配正则表达式的索引”;由 String.prototype.search()方法使用,这个方法会使用以 @@search 为键的函数来对正则表达式求值。

代码演示

正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:

 console.log(RegExp.prototype[Symbol.search]);
 // ƒ [Symbol.search]() { [native code] }
 console.log('SeaSound'.search(/und/));
 // 5

自定义

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象;如果想改变这种行为,让方法直接使用参数,可以重新定义 @@search 函数以取代默认对正则表达式求值的行为,从而让search()方法使用非正则表达式实例;

@@search 函数接收一个参数,就是调用 match()方法的字符串实例,返回的值没有限制;如下所示:

 class FooSearcher {
     static [Symbol.search](target) {
         // indexOf 用于查找元素的下标,没找到返回-1
         return target.indexOf('One');
     }
 }
 console.log('TestOne'.search(FooSearcher)); // 4 
 console.log('OneEgg'.search(FooSearcher)); // 0 
 console.log('Eggson'.search(FooSearcher)); // -1 
 ​
 ​
 class StringSearcher { 
  constructor(str) { 
  this.str = str; 
  } 
  [Symbol.search](target) { 
  return target.indexOf(this.str); 
  } 
 } 
 console.log('TestOne'.search(new StringSearcher('One'))); // 4 
 console.log('OneEgg'.search( new StringSearcher('One')) ); // 0 
 console.log('Eggson'.search( new StringSearcher('Son')) ); // -1 

 

Symbol.species

@@species 符号属性表示“一个函数值,该函数作为创建派生对象的构造函数”;这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法;

代码演示

@@species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义,如下所示:

 class One extends Array {} 
 ​
 let one = new One(); 
 console.log(one instanceof Array); // true 
 console.log(one instanceof One); // true 
 // String.concat为拼接
 one = one.concat('Ones'); 
 console.log(one instanceof Array); // true 
 console.log(one instanceof One); // true 
 console.log(one); // One ['Ones']
 ​
 // 使用获取器创建新的实例原型
 class Two extends Array { 
     static get [Symbol.species]() { 
        return Array; 
     } 
    } 
 ​
 let two = new Two(); 
 console.log(two instanceof Array); // true 
 console.log(two instanceof Two); // true 
 ​
 two = two.concat('Twos'); 
 console.log(two instanceof Array); // true 
 console.log(two instanceof Two); // false
 console.log(two); // ['Twos']

 

Symbol.split

@@split符号属性用于“在匹配正则表达式的索引位置拆分字符串”,由 String.prototype.split()方法使用;String.prototype.split()方法会使用以 @@split 为键的函数来对正则表达式求值。

代码演示

正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:

 console.log(RegExp.prototype[Symbol.split]);
 // ƒ [Symbol.split]() { [native code] }
 console.log('MusicBox'.split(/Box/));
 // ['Music','']

自定义

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象;如果想改变这种行为,让方法直接使用参数,可以重新定义 @@split 函数以取代默认对正则表达式求值的行为,从而让 split()方法使用非正则表达式实例。

@@split 函数接收一个参数,就是调用 match()方法的字符串实例,返回的值没有限制;如下所示:

 class FooSplitter { 
     static [Symbol.split](target) { 
         return target.split('Koo'); 
     } 
    } 
 console.log('TestKooUzz'.split(FooSplitter)); 
 // ['Test','Uzz']
 ​
 class StringSplitter { 
     constructor(str) { 
     this.str = str; 
     } 
     [Symbol.split](target) { 
         return target.split(this.str); 
     } 
 } 
 console.log('TestKooUzz'.split(new StringSplitter('Uzz'))); 
 // ['Test','Koo']

 

Symobl.toPrimitive

@@toPrimitive 符号属性用于“将对象转换为相应的原始值”;由 ToPrimitive 抽象操作使用,很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。

代码演示

正常的构造函数演示,如下所示:

 class Foo { }
 let foo = new Foo();
 console.log(3 + foo); // "3[object Object]" 
 console.log(3 - foo); // NaN 
 console.log(String(foo)); // "[object Object]" 

自定义

对于一个自定义对象实例,通过在这个实例的 @@toPrimitive 属性上定义一个函数可以改变默认行为根据提供给这个函数的参数(string、number 或 default),可以控制返回的原始值;如下所示:

 class Bar {
     constructor() {
         this[Symbol.toPrimitive] = function (hint) {
             switch (hint) {
                 case 'number':
                     return 10;
                 case 'string':
                     return 'Life is True';
                 case 'default':
                 default:
                     return 'Cheep Up';
             }
         }
     }
 }
 ​
 let bar = new Bar();
 console.log(3 + bar); // "3Cheep Up" 
 console.log(3 - bar); // -7
 console.log(String(bar)); // "Life is True"

 

Symbol.toStringTag

@@toStringTag符号属性用于“创建对象的默认字符串描述”,由内置方法 Object.prototype.toString()使用。

通过 toString() 方法获取对象标识时,会检索由 @@toStringTag 指定的实例标识符,默认为"Object"。

代码演示

 // 使用Set()集合
 let s = new Set();
 console.log(s); // Set(0) {} 
 console.log(s.toString()); // [object Set] 
 console.log(s[Symbol.toStringTag]); // Set 
 ​
 // 使用构造函数
 class Foo { }
 let foo = new Foo();
 console.log(foo); // Foo {} 
 console.log(foo.toString()); // [object Object] 
 console.log(foo[Symbol.toStringTag]); // undefined 

自定义

内置类型已经指定了这个值,但自定义类实例还需要明确定义,如下所示:

 class Bar {
     constructor() {
         this[Symbol.toStringTag] = 'Bar';
     }
 }
 let bar = new Bar();
 console.log(bar); // Bar {Symbol(Symbol.toStringTag): 'Bar'}
 console.log(bar.toString()); // [object Bar] 
 console.log(bar[Symbol.toStringTag]); // Bar

 

总结

内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为;

  • 内置符号属性都是不可写、不可枚举、不可配置的
  • Symbol.Iterator:为每一个对象定义了默认的迭代器
  • Symbol.asyncIterator:指定一个对象的默认异步迭代器
  • Symbol.hasInstance:检测某个对象是否是某个构造器的实例
  • Symbol.isConcatSpreadable:如果返回是 true,则对象打平其数组元素
  • Symbol.match:检索返回一个字符串匹配正则表达式的结果
  • Symbol.replace:替换一个字符串中匹配的子串
  • Symbol.search:返回字符串中正则表达式的索引
  • Symbol.species:该函数作为创建派生对象的构造函数
  • Symbol.split:在匹配正则表达式的索引位置拆分字符串
  • Symbol.toPrimitive:将对象转换为相应的原始值
  • Symbol.toStringTag:创建对象的默认字符串描述

关于更多Symbol属性与方法请查看 MDN-Symbol