跳到主要内容

Symbol 类型

Symbol (符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险

调用 Symbol() 函数时,可以传入一个字符串参数作为对符号的描述( description ),将来可以通 过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:

const genericSymbol = Symbol();
const otherGenericSymbol = Symbol();
const fooSymbol = Symbol('foo');
const otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false;
console.log(fooSymbol == otherFooSymbol); // false

符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建 Symbol() 实例并将其 用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

const genericSymbol = Symbol();
console.log(genericSymbol);
// Symbol()
const fooSymbol = Symbol('foo');
console.log(fooSymbol); // Symbol(foo);

最重要的是, Symbol() 函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符 号包装对象,像使用 Boolean 、 String 或 Number 那样,它们都支持构造函数且可用于初始化包含原 始值的包装对象:

const myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"
const myString = new String();
console.log(typeof myString); // "object"
const myNumber = new Number();
console.log(typeof myNumber); // "object"
const mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

Symbol.for() 对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运 行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同 字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

const fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
const otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); //true;

可以使用 Symbol.keyFor() 来查询全局注册表,这个方法接收符号,返回该全局符号对应的字 符串键。如果查询的不是全局符号,则返回 undefined 。

// 创建全局符号
const s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// // 创建普通符号
const s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
/* 如果传给 Symbol.keyFor() 的不是符号,则该方法抛出 TypeError :
*/
Symbol.keyFor(123); // TypeError: 123 is not a sy

使用符号作为属性​

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和 Object.defineProperty()/Object.defineProperties() 定义的属性。对象字面量只能在计算属 性语法中使用符号作为属性。

/**
* 浏览器环境执行的 node 环境执行不一致
*
*/
const s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
const 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() 返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。 Object.getOwnPropertyDescriptors() 会返回同时包含常规和符号属性描述符的对象。 Reflect.ownKeys() 会返回两种类型 的键:

const s1 = Symbol('foo'),
s2 = Symbol('bar');
const o = { [s1]: 'foo val', [s2]: 'bar val', baz: 'baz val', qux: 'qux val' };
/**
* Symbol(foo), Symbol(bar) ]
*/
console.log(Object.getOwnPropertySymbols(o));
/**
* [ 'baz', 'qux' ]
*/
console.log(Object.getOwnPropertyNames(o));
/**
* {
* baz: {
* value: 'baz val',
* writable: true,
* enumerable: true,
* configurable: true
* },
* qux: {
* value: 'qux val',
* writable: true,
* enumerable: true,
* configurable: true
* },
* [Symbol(foo)]: {
* value: 'foo val',
* writable: true,
* enumerable: true,
* configurable: true
* },
* [Symbol(bar)]: {
* value: 'bar val',
* writable: true,
* enumerable: true,
* configurable: true
* }
* }
*
*
*/
console.log(Object.getOwnPropertyDescriptors(o));
/**
* [ 'baz', 'qux', Symbol(foo), Symbol(bar) ]
*/
console.log(Reflect.ownKeys(o));

常用内置符号​

ECMAScript 6 也引入了一批常用内置符号( well-known symbol ),用于暴露语言内部行为,开发者 可以直接访问 、 重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。 这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道 for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义 Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。

这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号 的实例。所有内置符号属性都是不可写 、 不可枚举 、 不可配置的。

在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为 @@ 。比如, @@iterator 指的就是 Symbol.iterator

Symbol.asyncIterator​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个方法,该方法返回对象默认的 AsyncIterator 。 由 for-await-of 语句使用 "。换句话说,这个符号表示实现异步迭代器 API 的函数。

for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator 为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API 的 AsyncGenerator 。

/**
*
*/
class Foo {
/**
*
*/
async *[Symbol.asyncIterator]() {}
}
const f = new Foo();
console.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 => resolve(this.asyncIdx++));
}
}
}
/**
*
*/
async function asyncCount() {
const emitter = new Emitter(5);
for await (const x of emitter) {
console.log(x);
}
}
asyncCount();
// 0 // 1 // 2 // 3 // 4

Symbol.asyncIterator 是 ES2018 规范定义的,因此只有版本非常新的浏览器 支持它。

Symbol.hasInstance​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用 "。 instanceof 操作符可以用来确定一个对象 实例的原型链上是否有原型。

ES6 中, instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。以 Symbol. hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下。

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

这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用。由于 instanceof 操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其它属性一样,因此可以在继承的类上通 过静态方法重新定义这个函数。

/**
*
*/
class Bar {}
/**
*
*/
class Baz extends Bar {
/**
*
*/
static [Symbol.hasInstance]() {
return false;
}
}
const b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false

Symbol.isConcatSpreadable​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个布尔值,如果是 true ,则意味着对象应 该用 Array.prototype.concat() 打平其数组元素 "。 ES6 中的 Array.prototype.concat() 方法会 根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖 Symbol isConcatSpreadable 的值可以修改这个行为。

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

const initial = ['foo'];
const array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)]
const arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']
const otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]

otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo']

Symbol.iterator​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个方法,该方法返回对象默认的迭代器。 由 for-of 语句使用 "。换句话说,这个符号表示实现迭代器 API 的函数。 for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API 的 Generator 。

技术上,这个由 Symbol.iterator 函数生成的对象应该通过其 next() 方法陆续返回值。可以通 过显式地调用 next() 方法返回,也可以隐式地通过生成器函数返回。

Symbol.match​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个正则表达式方法,该方法用正则表达式 去匹配字符串。由 String.prototype.match() 方法使用 "。 String.prototype.match() 方法会使 用以 Symbol.match 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义, 因此所有正则表达式实例默认是这个 String 方法的有效参数。

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让 match() 方法使用非正则表达式实例。 Symbol.match 函数接收一个参数,就是调用 match() 方法的 字符串实例。但,返回的值没有限制 。

// ƒ [Symbol.match]() { [native code] }
console.log(RegExp.prototype[Symbol.match]);
// ["bar", index: 3, input: "foobar", groups: undefined]
console.log('foobar'.match(/bar/));
/**
*
*/
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
console.log('barbaz'.match(new StringMatcher('qux'))); // false

Symbol.replace​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个正则表达式方法,该方法替换一个字符 串中匹配的子串。由 String.prototype.replace() 方法使用 "。 String.prototype.replace() 方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函 数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。

给这个方法传入非正则表达式值会导致该值被转换为 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.str).join(replacement);
}
}
console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux'));
// "barquxbaz"

Symbol.search​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个正则表达式方法,该方法返回字符串中 匹配正则表达式的索引。由 String.prototype.search() 方法使用 String.prototype.search() 方法会使用以 Symbol.search 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数 的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,可以重新定义 Symbol.search 函数以取代默认对正则表达式求值的行为,从而让 search() 方法使用非正则表达式实例。 Symbol.search 函数接收一个参数,就是调用 match() 方法 的字符串实例。但,返回的值没有限制 。

Symbol.species​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个函数值,该函数作为创建派生对象的构 造函数 "。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方 法。用 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​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个正则表达式方法,该方法在匹配正则表 达式的索引位置拆分字符串。由 String.prototype.split() 方法使用 "。 String.prototype. split() 方法会使用以 Symbol.split 为键的函数来对正则表达式求值。正则表达式的原型上默认有 这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。

//ƒ [Symbol.split]() { [native code] }
console.log(RegExp.prototype[Symbol.split]);
// ['foo', 'baz']
console.log('foobarbaz'.split(/bar/));

给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,可以重新定义 Symbol.split 函数以取代默认对正则表达式求值的行为,从而让 split() 方法使用非正则表达式实例。 Symbol.split 函数接收一个参数,就是调用 match() 方法的字符串实 例。返回的值没有限制。

/**
*
*/
class FooSplitter {
/**
*
*/
static [Symbol.split](target) {
return target.split('foo');
}
}
console.log('barfoobaz'.split(FooSplitter));
// ["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​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个方法,该方法将对象转换为相应的原始 值。由 ToPrimitive 抽象操作使用 "。很多内置操作都会尝试强制将对象转换为原始值,包括字符串 、 数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性 上定义一个函数可以改变默认行为。

/**
*
*/
class Foo {}
const 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';
}
};
}
}
const bar = new Bar();
console.log(3 + bar); // "3default bar"
console.log(3 - bar); // 0
console.log(String(bar)); // "string bar"

Symbol.toStringTag​

根据 ECMAScript 规范,这个符号作为一个属性表示 "一个字符串,该字符串用于创建对象的默认 字符串描述。由内置方法 Object.prototype.toString() 使用 "。 通过 toString() 方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符,默 认为 "Object" 。内置类型已经指定了这个值,但自定义类实例还需要明确定义 。

const s = new Set();
console.log(s); // Set(0)
{
}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set class Foo {}
const 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';
}
}
const bar = new Bar();
console.log(bar); // Bar {}
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar

Symbol.unscopables​

这个符号作为一个属性表示 "一个对象,该对象所有的以及继承的属性, 都会从关联对象的 with 环境绑定中排除 "。设置这个符号并让其映射对应属性的键值为 true ,就可以 阻止该属性出现在 with 环境绑定中。

let o = { foo: 'bar' };
with (o) {
console.log(foo); // bar
}
o[Symbol.unscopables] = {
foo: true,
};
with (o) {
console.log(foo); // ReferenceError
}

不推荐使用 with ,因此也不推荐使用 Symbol.unscopables