JavaScript ES6 Symbol 对象

符号本身是原始类型,所以 typeof 操作符对符号返回 symbol。

1const sym = Symbol()
2console.log(typeof sym) // symbol

声明

  1. 原始数据类型,不能使用 new
  2. 括号内是备注的意思,独一无二的值,两个 Symbol 不相等
  3. typeof 检测为 Symbol
  4. 作为对象的键名独一无二、使用 [] 设置/获取,不能用.运算符获取设置
  5. 作为对象属性名是,是共有属性,不是私有属性,可以在类的外部访问
  6. 不能被枚举 for in、for of、Object.keys()、Object.getOwnPropertyNames() 返回
  7. 可以被 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 取到
1const sy = Symbol('name') // 不能用 new 命令,参数是备注
2Symbol('KK') == Symbol('KK') // false
3typeof Symbol('KK') // 'symbol'

作为属性名

1const sy = Symbol('name')
2const obj = {} // 设置symbol属性
3obj[sy] = 'kk'
4
5// 获取
6obj.sy // undefined 不能使用.运算符获取
7obj[sy] // kk

为了避免创建符号包装对象,Symbol()函数不能与 new 关键字一起作为构造函数使用。

1const myBoolean = new Boolean()
2console.log(typeof myBoolean) // "object"
3
4const myString = new String()
5console.log(typeof myString) // "object"
6
7const myNumber = new Number()
8console.log(typeof myNumber) // "object"
9
10const mySymbol = new Symbol() // TypeError: Symbol is not a constructor

如果要包装对象

1const mySymbol = Symbol()
2const myWrappedSymbol = Object(mySymbol)
3console.log(typeof myWrappedSymbol) // "object"
  • 全局符号注册表第一次调用会检查全局的注册表,不存在则生成新的符号添加到注册表,存在则会直接返回对于的。
1const fooGlobalSymbol = Symbol.for('foo') // 创建新符号
2const otherFooGlobalSymbol = Symbol.for('foo') // 重用已有符号

与 Symbol()定义的符号也并不等同

1const localSymbol = Symbol('foo')
2const globalSymbol = Symbol.for('foo')
3console.log(localSymbol === globalSymbol) // false

Symbol.for()的任何值都会被转换为字符串

1const emptyGlobalSymbol = Symbol.for()
2console.log(emptyGlobalSymbol) // Symbol(undefined)

Symbol.keyFor()来查询全局注册表

1// 创建全局符号
2const s = Symbol.for('foo')
3console.log(Symbol.keyFor(s)) // foo
4// 创建普通符号
5const s2 = Symbol('bar')
6console.log(Symbol.keyFor(s2)) // undefined
7// 如果传给Symbol.keyFor()的不是符号,则该方法抛出TypeError:
8Symbol.keyFor(123) // TypeError: 123 is not a symbol

计算属性语法中使用符号作为属性:

1const s1 = Symbol('foo')
2const s2 = Symbol('bar')
3const s3 = Symbol('baz')
4const s4 = Symbol('qux')
5Object.defineProperties(
6  {
7    [s1]: 'foo val',
8  },
9  {
10    [s3]: { value: 'baz val' },
11    [s4]: { value: 'qux val' },
12  }
13)
14console.log(o)
15// {Symbol(foo): foo val, Symbol(bar): bar val,
16// Symbol(baz): baz val, Symbol(qux): qux val}

如果没有保存 Symbol 的引用,需要遍历才能找到:

1let o = {
2  [Symbol('foo')]: 'foo val',
3  [Symbol('bar')]: 'bar val'
4};
5console.log(o);
6// {Symbol(foo): "foo val", Symbol(bar): "bar val"}
7let barSymbol = Object.getOwnPropertySymbols(o)
8.find((symbol) => symbol.toString().match(/bar/));
9console.log(barSymbol);
10// Symbol(bar)

Symbol.for()

1const sy = Symbol('a')
2console.log(Symbol.for('a')) // Symbol(a)
  1. 全局搜索是否存在该名称
  2. 有:返回本身,没有:新建一个返回
  3. 使两个 Symbol 类型的变量相等,生成同一个 Hash 值
1Symbol('Yellow') === Symbol.for('Yellow') // false
2Symbol.for('Yellow') === Symbol.for('Yellow') // true

Symbol.keyFor(sym)

  1. 查找键值的某个 Symbol
  2. 找到返回该 Symbol 的 key 值,字符串类型。否则 undefined
1const globalSym = Symbol.for('foo') // 创建一个全局 Symbol
2Symbol.keyFor(globalSym) // "foo"
3
4const localSym = Symbol()
5Symbol.keyFor(localSym) // undefined,
6
7// 以下Symbol不是保存在全局Symbol注册表中
8Symbol.keyFor(Symbol.iterator) // undefined

Symbol.asyncIterator

该方法返回对象默认的 AsyncIterator 由 for-await-of 语句使用。

1class Foo {
2  async *[Symbol.asyncIterator]() {}
3}
4const f = new Foo()
5console.log(f[Symbol.asyncIterator]())
6// AsyncGenerator {<suspended>}

技术上来说,由 Symbol.asyncIterator 函数生成的对象应该通过 next()方法陆续返回 Promise 实例。

也可以隐式通过异步生成器函数返回:

1class Emitter {
2  constructor(max) {
3    this.max = max
4    this.asyncIdx = 0
5  }
6
7  async *[Symbol.asyncIterator]() {
8    while (this.asyncIdx < this.max)
9      yield new Promise(resolve => resolve(this.asyncIdx++))
10  }
11}
12async function asyncCount() {
13  const emitter = new Emitter(5)
14  for await (const x of emitter) {
15    // next 调用后的 value
16    console.log(x)
17  }
18}
19asyncCount()
20// 0
21// 1
22// 2
23// 3
24// 4

Symbol.hasInstance

ECMAScript 规范,这个符号作为一个属性表示“一个方法,传入对象返回该对象是否是它的实例。

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

1class Bar {}
2const b = new Bar()
3console.log(b instanceof Bar) // true

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

1class Bar {}
2const b = new Bar()
3console.log(Bar[Symbol.hasInstance](b)) // true

Baz 继承了 Bar 并覆写了 Symbol.hasInstance,instanceof 操作符也会在原型链上寻找这个属性,所以说就就跟在原型链上寻找其他属性一样,这里重新定义了这个函数

1class Bar {}
2class Baz extends Bar {
3  static [Symbol.hasInstance]() {
4    return false;
5  }
6}
7let b = new Baz();
8console.log(Bar[Symbol.hasInstance](b)); // true
9console.log(b instanceof Bar); // true
10console.log(Baz[Symbol.hasInstance](b)); // false 调用 Baz 覆写后的 Symbol.hasInstance
11console.log(b instanceof Baz); // false 同上,返回的值是转换为 Boolean

Symbol.isConcatSpreadable

  • ECMAScript 规范,这个符号作为一个属性表示“一个布尔值”
  • 默认情况下则对象应该用 Array.prototype.concat()展开数组元素
  • 如果是 true,则被忽略
1const initial = ['foo']
2const array = ['bar']
3console.log(array[Symbol.isConcatSpreadable]) // undefined
4console.log(initial.concat(array)) // ['foo', 'bar']
5array[Symbol.isConcatSpreadable] = false
6console.log(initial.concat(array)) // ['foo', Array(1)]
7
8const arrayLikeObject = { length: 1, 0: 'baz' }
9console.log(arrayLikeObject[Symbol.isConcatSpreadable]) // undefined
10console.log(initial.concat(arrayLikeObject)) // ['foo', {...}]
11arrayLikeObject[Symbol.isConcatSpreadable] = true
12console.log(initial.concat(arrayLikeObject)) // ['foo', 'baz']
13
14const otherObject = new Set().add('qux')
15console.log(otherObject[Symbol.isConcatSpreadable]) // undefined
16console.log(initial.concat(otherObject)) // ['foo', Set(1)]
17otherObject[Symbol.isConcatSpreadable] = true
18console.log(initial.concat(otherObject)) // ['foo']

Symbol.iterator

  • ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器,由 for-of 语句使用”
  • for-of 循环时,会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。
  • 很多时候,返回的对象是实现该 API 的 Generator:
1class Foo {
2  *[Symbol.iterator]() {}
3}
4const f = new Foo()
5console.log(f[Symbol.iterator]())
6// Generator {<suspended>}

Symbol.iterator 返回的对象可以通过 next()方法陆续返回值。也可以隐式地通过生成器函数返回:

1class Emitter {
2  constructor(max) {
3    this.max = max;
4    this.idx = 0;
5  }
6  *[Symbol.iterator]() {
7    while(this.idx < this.max) {
8      yield this.idx++;
9    }
10  }
11}
12function count() {
13  let emitter = new Emitter(5);
14  for (const x of emitter) {
15    console.log(x);
16  }
17}
18count();
19// 0
20// 1
21// 2
22// 3
23// 4

Symbol.match

  • ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”
  • String.prototype.match() 会调用以 Symbol.match 为键的函数来正则求值
  • 正则表达式的原型上就有 Symbol.match 这个函数
1console.log(RegExp.prototype[Symbol.match])
2// ƒ [Symbol.match]() { [native code] }
3console.log('foobar'.match(/bar/))
4// ["bar", index: 3, input: "foobar", groups: undefined]

match 方法传入非正则表达式会被转为 RegExp 对象,如果要改变默认行为,需要重新定义 Symbol.match 函数。

Symbol.match 接收一个参数,就是调用 match() 方法之前的字符串实例。

1class FooMatcher {
2  static [Symbol.match](target) {
3    return target.includes('foo');
4  }
5}
6console.log('foobar'.match(FooMatcher)); // true
7console.log('barbaz'.match(FooMatcher)); // false
8class StringMatcher {
9  constructor(str) {
10    this.str = str;
11  }
12  [Symbol.match](target) {
13    return target.includes(this.str);
14  }
15}
16console.log('foobar'.match(new StringMatcher('foo'))); // true
17console.log('barbaz'.match(new StringMatcher('qux'))); // false

Symbol.replace

  • 这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”
  • String.prototype.replace() 会调用 Symbol.replace 为键的方法来正则求值
  • 正则表达式原型上也有 Symbol.replace
1console.log(RegExp.prototype[Symbol.replace])
2// ƒ [Symbol.replace]() { [native code] }
3console.log('foobarbaz'.replace(/bar/, 'qux'))
4// 'fooquxbaz'

这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象,可以重新定义 Symbol.replace 函数取代默认对正则表达式求值的行为。

Symbol.replace 函数接收两个参数,即调用 replace()方法的字符串实例和替换字符串

1class FooReplacer {
2  static [Symbol.replace](target, replacement) {
3    return target.split('foo').join(replacement);
4  }
5}
6console.log('barfoobaz'.replace(FooReplacer, 'qux')); // "barquxbaz"
7class StringReplacer {
8  constructor(str) {
9    this.str = str;
10  }
11  [Symbol.replace](target, replacement) {
12    return target.split(this.str).join(replacement);
13  }
14}
15console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); // "barquxbaz"

Symbol.search

  • ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search()方法使用”
  • String.prototype.search() 会调用 Symbol.search 为键的方法来正则求值
  • 正则表达式原型上也有 Symbol.search
1console.log(RegExp.prototype[Symbol.search])
2// ƒ [Symbol.search]() { [native code] }
3console.log('foobar'.search(/bar/)) // 3

search 方法传入非正则表达式值会被转换为 RegExp 对象,重新定义 Symbol.search 函数以取代默认对正则表达式求值的行为

Symbol.search 函数接收一个参数,就是调用 search()方法

1class FooSearcher {
2  static [Symbol.search](target) {
3    return target.indexOf('foo');
4  }
5}
6console.log('foobar'.search(FooSearcher)); // 0
7console.log('barfoo'.search(FooSearcher)); // 3
8console.log('barbaz'.search(FooSearcher)); // -1
9class StringSearcher {
10  constructor(str) {
11    this.str = str;
12  }
13  [Symbol.search](target) {
14    return target.indexOf(this.str);
15  }
16}
17console.log('foobar'.search(new StringSearcher('foo'))); // 0
18console.log('barfoo'.search(new StringSearcher('foo'))); // 3
19console.log('barbaz'.search(new StringSearcher('qux'))); // -1

Symbol.species

  • ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。
1class Bar extends Array {}
2class Baz extends Array {
3  static get [Symbol.species]() {
4    return Array;
5  }
6}
7let bar = new Bar();
8console.log(bar instanceof Array); // true
9console.log(bar instanceof Bar); // true
10bar = bar.concat('bar');
11console.log(bar instanceof Array); // true
12console.log(bar instanceof Bar); // true
13
14let baz = new Baz();
15console.log(baz instanceof Array); // true
16console.log(baz instanceof Baz); // true
17baz = baz.concat('baz');
18console.log(baz instanceof Array); // true
19console.log(baz instanceof Baz); // false

刚看到这有点懵,看下面例子:

1class MyArr extends Array {}
2const a = new MyArr(1, 2, 3)
3const b = a.map(item => item + 1)
4b instanceof MyArr // true
  1. MyArr 继承 Array,a 是 MyArr 构造的实例,b 又是 a 实例衍生的对象。
  2. 按道理说 b 应该是 Array 构造出来的实例,但实际上 MyArr 构造的实例(instanceof 返回 true 说明 b 原型链上存在 MyArr)
  3. 而 Symbol.species 属性就是为了解决这个问题而提供的:
1class MyArray extends Array {
2  static get [Symbol.species]() {
3    return Array
4  }
5}
6const a = new MyArray(1, 2, 3)
7const b = a.filter(x => x < 5)
8Array.isArray(b) // true
9b instanceof MyArray // false

这样,衍生出来的 b 则不是 MyArray 构造出来的了,而是 return 返回 Array 的实例

Symbol.split

  • ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,方法匹配正则的索引位置拆分字符串。由 String.prototype.split()方法使用”。
  • String.prototype.split()方法会使用以 Symbol.split 为键的函数来对正则表达式求值
  • 正则表达式原型上也有 Symbol.split
1console.log(RegExp.prototype[Symbol.split])
2// ƒ [Symbol.split]() { [native code] }
3console.log('foobarbaz'.split(/bar/))
4// ['foo', 'baz']

Symbol.split 第一个参数就是调用 match()方法的字符串实例。

1class FooSplitter {
2  static [Symbol.split](target) {
3    return target.split('foo');
4  }
5}
6console.log('barfoobaz'.split(FooSplitter)); // ["bar", "baz"]
7class StringSplitter {
8  constructor(str) {
9    this.str = str;
10  }
11  [Symbol.split](target) {
12    return target.split(this.str);
13  }
14}
15console.log('barfoobaz'.split(new StringSplitter('foo'))); // ["bar", "baz"]

Symbol.toPrimitive

  • ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive 抽象操作使用”
  • 很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型
  • 重写 Symbol.toPrimitive 属性上的方法可以改变默认行为:
1class Foo {}
2let foo = new Foo();
3console.log(3 + foo); // "3[object Object]"
4console.log(3 - foo); // NaN
5console.log(String(foo)); // "[object Object]"
6class Bar {
7  constructor() {
8    this[Symbol.toPrimitive] = function(hint) {
9      switch (hint) {
10        case 'number':
11          return 3;
12        case 'string':
13          return 'string bar';
14        case 'default':
15        default:
16          return 'default bar';
17      }
18    }
19  }
20}
21let bar = new Bar();
22console.log(3 + bar); // "3default bar"
23console.log(3 - bar); // 0
24console.log(String(bar)); // "string bar"

Symbol.toStringTag

  • ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”。
  • toString()获取对象标识时,会查找 Symbol.toStringTag 指定的实例标识符,默认为"Object"。
1const s = new Set()
2console.log(s) // Set(0) {}
3console.log(s.toString()) // [object Set]
4console.log(s[Symbol.toStringTag]) // Set

内置类型已经定义了 Symbol.toStringTag 属性,但自定义类需要自己定义:

1class Foo {}
2let foo = new Foo();
3console.log(foo); // Foo {}
4console.log(foo.toString()); // [object Object]
5console.log(foo[Symbol.toStringTag]); // undefined
6class Bar {
7  constructor() {
8    this[Symbol.toStringTag] = 'Bar';
9  }
10}
11let bar = new Bar();
12console.log(bar); // Bar {}
13console.log(bar.toString()); // [object Bar]
14console.log(bar[Symbol.toStringTag]); // Bar

Symbol.unscopables

  • ECMAScript 规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除”
  • 让对应的属性值的键为 true,就可以阻止改属性出现在 with 中
1let o = { foo: 'bar' };
2with (o) {
3  console.log(foo); // bar
4}
5o[Symbol.unscopables] = {
6  foo: true
7};
8with (o) {
9  console.log(foo); // ReferenceError
10}