JavaScript中的Symbol

Symbol类型:

  • ES6新增数据类型。符号是原始值,且符号实例是唯一、不可变的。

  • 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

  • symbol并不是为了提供私有属性的行为而新增的。

  • 符号就是用来·创建唯一记号,进而用作非字符串形式的对象属性。

  • symbol的基本使用:

    • 符号使用Symbol()函数初始化。因为符号本身是原始类型,typeof返回’symbol’
      let sym = Symbol();
      console.log(typeof sym); // 'symbol'
    
    • 调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),方便日后通过这个字符串来调试代码。
    • 这个字符串参数与符号定义或标识完全无关。
      let genericSymbol = Symbol();
      let otherGenericSymbol = Symbol();
    
      let fooSymbol = Symbol('foo');
      let otherFooSymbol = Symbol('foo');
    
      console.log(genericSymbol == otherGenericSymbol); // false
      console.log(fooSymbol == otherFooSymbol) // false
    
  • 【重要】Symbol()函数不能与new关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象。

  • Boolean、String、Number都支持构造函数且可用于初始化包含原始值的包装对象:

  let myBoolean = new Boolean();
  console.log(typeof myBoolean); // 'object'

  let myString = new String();
  console.log(typeof myString); // "object"

  let myNumber = new Number();
  console.log(typeof myNumber); // "object"

  let mySymbol = new Symbol(); // typeerror
  • 但可以使用Object()函数来实现符号包装对象:
  let my Symbol = Symbol();
  let myws = Object(mySymbol);
  typeof myws; // 'object'
  • 使用全局符号注册表

    • 如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符作为键,在全局符号注册表中创建并重用符号。

    • 使用Symbol.for()方法

    • Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,若不存在则生成一个新符号实例并添加到注册表中。后续使用发现之,返回之。

    • 即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不完全等同。

    • 全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()的任何值都会被转换成字符串。

    • 此外,注册表中使用的键同时也会被用作符号描述。

      let emptyGS = Symbol.for();
      emptyGS // Symbol(undefined) 
    
    • Symbol.keyFor(value):查询全局注册表;value是符号,返回该全局符号对应的字符串键。
    • 若查询的不是全局符号,返回undefined;
    • 若value不是符号,则抛出TypeError;
      let s = Symbol.for('foo'); // 全局符号
      let s2 = Symbol('bar'); // 普通符号
    
      Symbol.keyFor(s); // foo
      Symbol.keyFor(s2); // undefined
    
  • 使用符号作为属性

    • 凡是可以使用字符串或数值作为属性的地方,都可以使用符号。
    • 包括对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性。
    • 对象字面量只能在计算属性语法中使用符号作为属性。
      let s1 = Symbol('foo'),
      s2 = Symbol('bar'),
      s3 = Symbol('baz'),
      s4 = Symbol('qux');
      let o = {
      [s1]: 'foo val'
      };
      // 这样也可以:o[s1] = 'foo val';
      console.log(o);
      // {Symbol(foo): foo val}
      Object.defineProperty(o, s2, {value: 'bar val'});
      console.log(o);
      // {Symbol(foo): foo val, Symbol(bar): bar val}
      Object.defineProperties(o, {
      [s3]: {value: 'baz val'},
      [s4]: {value: 'qux val'}
      });
      console.log(o);
      // {Symbol(foo): foo val, Symbol(bar): bar val,
      // Symbol(baz): baz val, Symbol(qux): qux val}
    
    • Object.getOwnPropertyNames():返回对象实例的常规属性数组
    • Object.getOwnPropertySymbols():返回对象实例的符号属性数组。
    • 这2个方法的返回值彼此互斥。
    • Object.getOwnPropertyDescriptions():返回同时包含常规和符号属性描述的对象。
    • Reflect.ownKeys():返回2种类型的键。
      let s1 = Symbol('foo'),
          s2 = Symbol('bar');
      
      let o = {
        [s1]: 'foo val',
        [s2]: 'bar val',
        baz: 'baz val',
        qux: 'qux val'
      };
    
      console.log(Object.getOwnPropertySymbols(o));
      // [Symbol(foo), Symbol(bar)]
      console.log(Object.getOwnPropertyNames(o));
      // ["baz", "qux"]
      console.log(Object.getOwnPropertyDescriptors(o));
      // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
      console.log(Reflect.ownKeys(o));
      // ["baz", "qux", Symbol(foo), Symbol(bar)]
    
    • 因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。
    • 若没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键。
      let o = {
        [Symbol('foo')]: 'foo val',
        [Symbol('bar')]: 'bar val'
      }
    
      console.log(o); // {Symbol(foo): 'foo val', Symbol(bar): 'bar val'}
    
      let barSymbol = Object.getOwnPeopertySymbols(o).find((symbol) => symbol.toString().match(/bar/))
      console.log(barSymbol) // Symbol(bar)
    
  • 常用内置符号

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

    • 这些内置符号都以Symbol工厂函数数字字符串属性的形式存在。

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

    • 例如:

      • for-of循环会在对象上使用Symbol.iterator属性。那么就可以在自定义对象上重新定义Symbol.iterator的值,来改变for-of在迭代该对象时的行为。
    • 所有内置符号属性都是不可写、不可枚举、不可配置的。

  • Symbol.asyncIterator

    • 这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator。由for-await-of语句使用”。
    • 这个符号其实就是表示实现异步迭代器API的函数。
    • for-await-of循环会利用这个函数执行异步迭代操作。循环时,它们调用以Symbol.asyncIterator为键的函数,并期望这个函数返回一个实现迭代器API的对象。很多时候,返回的对象是实现该API的AsyncGenerator:
      class Foo {
        async *[Symbol.asyncIterator]() {}
      }
    
      let f = new Foo();
    
      consoel.log(f[Symbol.asyncIterator]()); // AsyncGenerator {<suspended>}
    
    • 技术上,这个由Symbol.asyncIterator函数生成的对象应该通过其next()方法陆续返回Promise实例。
    • 可以通过显示地调用next()方法返回,也可以隐式地通过异步生成器函数返回:
    class Emitter {
      constructor (max) {
        this.max = max;
        this.asyncIdx = 0;
      }
    
      async *[Symbol.asyncIterator]() {
        while (this.asyncIdx < this.max) {
          yield new Promise((resolve, reject) => resolve(this.asyncIdx));
        }
      }
    }
    
    async function asyncCount () {
      let emitter = new Emitter(5);
    
      for await (const x of emitter) {
        console.log(x);
      }
    }
    
    // 0
    // 1
    // 2
    // 3
    // 4
    
    • Symbol.asyncIterator是ES2018规范定义的,只有版本非常新的浏览器支持它
  • Symbol.hasInstance

    • 这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由instanceof操作符使用”
    • instanceof操作符可以用来确定一个对象实例的原型链上是否有原型。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
    
    • 在ES6中,instanceof操作符会使用Symbol.hasInstance函数来确认关系。
      function Foo () {}
      let f = new Foo()
      Foo[Symbol.hasInstance](f); // true
    
      class Bar {}
      let b = new Bar();
      console.log(Bar[Symbol.hasInstance](b)); // true
    
      class Bar {}
      class Baz extends Bar {
        static [Symbol.hasInstance]() {
          return false
        }
      }
    
      let b = new Baz();
      Bar[Symbol.hasInstance](b); // true
      b instanceof Bar; // true
      Baz[Symbol.hasInstance](b); // false
      b instanceof Baz; // false
    
  • Symbol.isConcatSpreadable

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

    • 这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由for-of语句使用”
  • Symbol.match

    • 根据 ECMAScript规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”。
    • String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义。
      RegExp.prototype[Symbol.match] // f [Symbol.match] () { [native code] }
    
      console.log('foobar'.match(/bar/));
      // ["bar", index: 3, input: "foobar", groups: undefined]
    
    • 给这个方法传入非正则表达式会导致该值被转换成RegExp对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例。
    • 返回值没有限制:
      class FooMatcher {
        static [Symbol.match] (target) {
          return target.includes('foo');
        }
      }
    
      console.log('foobar'.match(FooMatcher)); // true
      console.log('barbaz'.match(FooMatcher)); // false
    
      class StringMatcher {
        constructor (str) {
          this.str = str;
        }
    
        [Symbol.match] (target) {
          return target.includes(this.str);
        }
      }
    
      console.log('foobar'.match(new StringMatcher('foo'))); // true
    
  • Symbol.replace

    • 根据ES规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串”。由String.prototype.replace()方法使用。
    • String.prototype.replace()方法回使用以Symbol.replace为键的函数来对正则表达式求值。
    • 正则表达式的原型上默认有这个函数的定义:
      console.log(RegExp.prototype[Symbol.replace]); // f [Symbol.replace]() [native code]
    
      console.log('foobarbaz'.replace(/bar/, 'qux')); // 'fooquxbaz
    
    • 给这个方法传入非正则表达式值会导致该值被转换为RegExp对象。
    • 要想改变这种行为,让方法直接使用参数,可以重新定义Symbol.replace函数以取代默认对正则表达式求值的行为,从而让replace()使用非正则表示式实例。
    • Symbol.replace函数接收两个参数,即调用replace()方法的字符串实例和替换字符串。返回的值没有限制:
      class FooReplacer {
        static [Symbol.replace] (target, replacement) {
          return target.split('foo').join(replacement);
        }
      }
    
      console.log('barfoobaz'.replace(FooReplacer, 'qux'));
      // "barquxbaz"
    
      class StringReplacer {
        constructor(str) {
          this.str = str;
        }
    
        [Symbol.replace] (target, replacement) {
          return target.split(this.target.split(this.str).join(replacement));
        }
      }
    
      console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux'));
      // "barquxbaz"
    
  • Symbol.search

    • 根据ES规范,这个符号作为一个属性表示“一个正则表达式方法,此方法返回字符串中匹配正则表达式的索引。由String.prototype.search()方法使用”
    • String.prototype.search()方法会使用Symbol.search为键的函数来对正则表达式进行求值。
    • 正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:
      console.log(RegExp.prototype[Symbol.search]);
      // f [SYmbol.search] () { [native code]}
    
      console.log('foobar'.search(/bar/)); // 3
    
    
    • 给这个方法传入非正则表达式会导致该值被转换成RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.search函数以取代默认对正则表达式求值的行为,从而让search()方法使用非正则表达式实例。
    • Symbol.search函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制:
      class FooSearcher {
        static [Symbol.search] (target) {
          return target.indexOf('foo');
        }
      }
      console.log('foobar'.search(FooSearcher)); // 0
      console.log('barfoo'.search(FooSearcher)); // 3
      console.log('barbaz'.search(FooSearcher)); // -1; 表示没有找到
    
      class StringSearcher {
        constructor (str) {
          this.str = str
        }
    
        [Symbol.search] (target) {
          return target.indexOf(this.str);
        }
      }
    
      console.log('foobar'.search(new StringSearcher('foo'))); // 0
    
  • Symbol.species

    • 根据ES规定,这个符号作为一个属性表示“一个函数值,该函数值作为创建派生对象的构造函数。”
    • 这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。
    • 用Symbol.species定义静态的获取器(getter)方法,可以覆盖创建实例的原型定义:
      class Bar extends Array {}
      class Baz extends Array {
        static get [Symbol.species]() {
          return Array;
        }
      }
    
      let bar = new Bar();
      console.log(bar instanceof Array); // true
      console.log(bar instanceof Bar); // true
      bar = bar.concat('bar');
      console.log(bar instanceof Array); // true
      console.log(bar instanceof Bar); // true
    
      let baz = new Baz();
      console.log(baz instanceof Array); // true
      console.log(baz instanceof Baz); // true
      baz = baz.concat('baz');
      console.log(baz instanceof Array); // true
      console.log(baz instanceof Baz); // false
    
  • Symbol.split

    • ES规定,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由String.prototype.split()方法使用”。
    • String.prototype.split()方法使用Symbol.split为键的函数来对正则表达式求值。正则表达式的原型上默认由这个函数的定义:
      console.log(RegExp.prototype[Symbol.split]);
      // f [Symbol.split] () { [native code] }
    
      console.log('foobarbaz'.split(/baz/));
      // ['foo', 'baz']
    
    • 给这个方法传入非正则表达式会导致该值被转换成RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.split函数以取代默认对正则表达式求值的行为,从而让方法直接使用非正则表达式实例。
      class FooSplitter {
        static [Symbol.split] (target) {
          return target.split('foo');
        }
      }
    
      console.log('barfoobaz'.split(FooSpliter));
      // ["bar", "baz"]
    
      class StringSplitter {
        constructor (str) {
          this.str = str
        }
        [Symbol.split] (target) {
          return target.split(this.str)
        }
      }
    
      console.log('barfoobaz'.split(new StringSplitter('foo')));
      // ["bar", "baz"]
    
  • Symbol.toPrimitive

    • ES规定,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由ToPrimitive抽象操作使用”。
    • 很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值、未指定的原始类型。
    • 对于一个自定义对象实例,通过在这个实例的Symbol.toPrimitive属性上定义一个函数可以改变默认行为。
    • 根据提供给这个函数的参数(string、number或default),可以控制返回值的原始值:
      class Foo {}
      let foo = new Foo();
    
      console.log(3 + foo); // "3[object Object]"
      console.log(3 - foo); // NaN
      console.log(String(foo)); // "[object Object]"
    
      class Bar {
        constructor() {
          this[Symbol.toPrimitive] = function(hint) {
            switch(hint) {
              case 'number':
                return 3;
              case 'string':
                return 'string bar';
              case 'default':
              default:
              return 'default bar';
            }
          }
        }
      }
    
      let bar = new Bar();
    
      console.log(3 + bar); // "3default bar"
      console.log(3 - bar); // 3-3 => "0"
      console.log(String(bar)); // "string bar"
      Number(bar); // 3
    
    
  • Symbol.toStringTag

    • 根据ES规定,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法Object.prototype.toString()方法使用”
    • 通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为“Object”。内置类型已经指定了这个值,但自定义类实例还需要明确定义:
      let s = new Set();
    
      s // Set(0) {}
      s.toString() // "[object Set]"
      s[Symbol.toStringTag]; // "Set"
    
      class Foo {}
      let foo = new Foo();
    
      foo // Foo {}
      foo.toString() // [object Pbject]
      foo[Symbol.toStringTag] // undefined
    
      class Bar {
        constructor() {
          this[Symbol.toStringTag] = 'Bar';
        }
      }
    
      let bar = new Bar();
    
      console.log(bar); // Bar {}
      bar.toString() // [object Bar]
      bar.[Symbol.toStringTag] // Bar
    
  • Symbol.unscopables

    • 根据ES,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的with环境百年孤独中排除”
    • 设置这个符号并让其映射对应属性的键值为true,就可以阻止该属性出现在with环境绑定中:
      let o = {
        foo: 'bar'
      };
      with (o) {
        console.log(foo); // bar
      }
    
      o[Symbol.unscopables] = {
        foo: true
      }
      width (o) {
        console.log(foo); // ReferenceError
      }
    
    • 不推荐使用 with,因此也不推荐使用 Symbol.unscopables。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值