本文内容主要来自《JavaScript高级程序设计(第4版)》章节 3.4.7,并结合MDN文档中的 Symbol 部分、阮一峰的《ECMASctipt 6 入门》章节13进行整理所得。
是一种原始数据类型,表示独一无二的值
const s1 = Symbol('test');
const s2 = Symbol('test');
console.log(s1 === s2); // false
console.log(Symbol() === Symbol()); // false
引入背景:避免对象属性冲突
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。
Symbol 实例的描述
Symbol() 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。
使用 Symbol.prototype.description 可以返回 Symbol 实例的描述。
作为对象的属性名的 Symbol
凡是可以使用字符串或数值作为属性的地方, 都可以使用 Symbol。 这就包括了对象字面量属性和 Object.defineProperty() / Object.defineProperties() 定义的属性。
不过,作为对象的属性名的 Symbol 值无法被这些方法遍历:for...in、for...of、Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()。
如果需要遍历,可以使用以下方法。
遍历方法:Object.getOwnPropertySmybols()、Object.getOwnPropertyDescriptors()、Reflect.ownkeys()
Object.getOwnPropertySmybols() 方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
注意,和该方法类似的 Object.getOwnPropertyNames() 只返回对象实例的常规属性数组。这两个方法的返回值彼此互斥。
Object.getOwnPropertyDescriptors() 方法返回同时包含常规和符号属性描述符的对象。
Reflect.ownkeys() 方法返回所有类型的键名,包括常规键名和 Symbol 键名。
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
全局注册:Symbol.for()、Symbol.keyFor()
有时,我们希望重新使用同一个 Symbol 值,Symbol.for() 方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的key。
注意: Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。Symbol.for()的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值。
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true
// 这意味着,iframe 窗口生成的 Symbol 值,可以在主页面得到。
常用内置符号(well-known symbol)
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。这些常用内置符号(well-known symbol)用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。
这些内置符号最重要的用途之一是重新定义它们, 从而改变原生结构的行为。
注意: 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。
Symbol.asyncIterator
这个符号作为一个属性表示“一个方法,该方法返回对象默认的 AsyncIterator。 由 for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。
Symbol.iterator
这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。 由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。
Symbol.hasInstance
这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”。
Symbol.species
这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。
举几个例子:
// 默认行为
class MyArray1 extends Array {
}
const a = new MyArray1(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof MyArray1 // true
c instanceof MyArray1 // true
// 修改默认行为后
class MyArray2 extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new MyArray2();
const b = a.map(x => x);
b instanceof MyArray2 // false
b instanceof Array // true
// 另一个例子
// T2定义了Symbol.species属性,T1没有。
// 结果就导致了创建衍生对象时(then方法),
// T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。
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
总之,Symbol.species 的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。
Symbol.isConcatSpreadable
作用:用于配置某对象作为 Array.prototype.concat() 方法的参数时是否展开其数组元素。 对不同数据有不同的默认行为:
- 对于数组,默认展开,但Symbol.isConcatSpreadable默认等于undefined
- 对于类数组 (array-like) 对象,默认不展开。期望展开其元素用于连接,需要设置 Symbol.isConcatSpreadable 为 true。
Symbol.match
有 2 种作用:
-
重新定义从 String 继承的 match() 方法的行为
class MyMatcher { [Symbol.match](string) { return 'hello world'.indexOf(string); } } 'e'.match(new MyMatcher()) // 1 -
指定了匹配的是正则表达式而不是字符串
// 默认行为:未修改 Symbol.match "/bar/".startsWith(/bar/); // Throws TypeError,因为 /bar/ 是一个正则表达式 // 修改 Symbol.match 为 false, // 表示将 re 视为字符串而不是正则表达式 var re = /foo/; re[Symbol.match] = false; "/foo/".startsWith(re); // true "/baz/".endsWith(re); // false
Symbol.replace
这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符 串中匹配的子串。由 String.prototype.replace()方法使用”。
Symbol.search
这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中 匹配正则表达式的索引。由 String.prototype.search()方法使用”。
Symbol.split
这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表 达式的索引位置拆分字符串。 由 String.prototype.split() 方法使用”。
Symbol.toPrimitive
指定了一种接受首选类型并返回对象原始值的表示的方法。它被所有的强类型转换制算法优先调用。
很多内置操作都会尝试强制将对象转换为原始值,包括字符串、 数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性 上定义一个函数可以改变默认行为。
该函数被调用时,会被传递一个字符串参数 hint,表示要转换到的原始值的预期类型。hint 参数的取值是 "number"、"string" 和 "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
这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”。
也就是说,这个属性可以用来定制 [object Object] 或 [object Array] 中 object 后面的那个大写字符串。
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'
Symbol.unscopables(不推荐使用)
这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性, 都会从关联对象的 with 环境绑定中排除”。
注意:由于不推荐使用 with,因此也不推荐使用 Symbol.unscopables。